From 8ff542405c7032c2e40f334316b2ae47476f5be2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 31 May 2022 17:41:02 +1000 Subject: [PATCH] Finished of the Conversation screen and JobQueue concurrency Updated the migrations to indicate progress (Potential to base progress for the "processing" sections on the file size of the legacy database) Updated the JobRunner to properly support concurrent queues for sending/receiving (other queues are still serial) Added the typing indicator logic into the ConversationVC Put code into SUKLegacy for connecting to the YDB database Fixed a couple of minor UI bugs with the GalleryRailView Updated the media gallery selection screen to use the appropriate system theme colouring (was painful to randomly swap from dark mode to like for one screen...) Added an alert for when the database migration fails Deleted the legacy migrations (manually applying any unapplied changes as part of the YDB to GRDB migration process) --- Session.xcodeproj/project.pbxproj | 80 --- Session/Conversations/ConversationVC.swift | 1 + .../Conversations/ConversationViewModel.swift | 30 +- .../OWSConversationSettingsViewController.m | 1 - Session/Home/HomeVC.swift | 27 +- Session/Home/HomeViewModel.swift | 95 ++- .../MessageRequestsViewModel.swift | 5 + .../ImagePickerController.swift | 31 +- .../MediaGalleryViewModel.swift | 5 + .../SendMediaNavigationController.swift | 4 - Session/Meta/AppDelegate.swift | 27 +- Session/Meta/MainAppContext.m | 1 - Session/Meta/Session-Prefix.pch | 3 - Session/Meta/Signal-Bridging-Header.h | 3 - .../Translations/de.lproj/Localizable.strings | 2 + .../Translations/en.lproj/Localizable.strings | 2 + .../Translations/es.lproj/Localizable.strings | 2 + .../Translations/fa.lproj/Localizable.strings | 2 + .../Translations/fi.lproj/Localizable.strings | 2 + .../Translations/fr.lproj/Localizable.strings | 2 + .../Translations/hi.lproj/Localizable.strings | 2 + .../Translations/hr.lproj/Localizable.strings | 2 + .../id-ID.lproj/Localizable.strings | 2 + .../Translations/it.lproj/Localizable.strings | 2 + .../Translations/ja.lproj/Localizable.strings | 2 + .../Translations/nl.lproj/Localizable.strings | 2 + .../Translations/pl.lproj/Localizable.strings | 2 + .../pt_BR.lproj/Localizable.strings | 2 + .../Translations/ru.lproj/Localizable.strings | 2 + .../Translations/si.lproj/Localizable.strings | 2 + .../Translations/sk.lproj/Localizable.strings | 2 + .../Translations/sv.lproj/Localizable.strings | 2 + .../Translations/th.lproj/Localizable.strings | 2 + .../vi-VN.lproj/Localizable.strings | 2 + .../zh-Hant.lproj/Localizable.strings | 2 + .../zh_CN.lproj/Localizable.strings | 2 + .../PrivacySettingsTableViewController.m | 1 - Session/Settings/ShareLogsModal.swift | 11 +- .../_001_InitialSetupMigration.swift | 3 +- .../Migrations/_002_SetupStandardJobs.swift | 3 +- .../Migrations/_003_YDBToGRDBMigration.swift | 672 ++++++++++-------- .../Database/Models/RecipientState.swift | 2 - .../Database/Models/SessionThread.swift | 38 +- .../Models/ThreadTypingIndicator.swift | 2 +- .../Meta/SessionMessagingKit.h | 1 - .../Sending & Receiving/Pollers/Poller.swift | 12 +- .../Shared Models/MessageViewModel.swift | 77 +- .../SessionThreadViewModel.swift | 28 +- .../Utilities/OWSWindowManager.m | 1 - .../SignalShareExtension-Bridging-Header.h | 2 - .../ThreadPickerViewModel.swift | 5 + .../_001_InitialSetupMigration.swift | 3 +- .../Migrations/_002_SetupStandardJobs.swift | 3 +- .../Migrations/_003_YDBToGRDBMigration.swift | 115 ++- .../Database/GRDBStorage.swift | 1 + .../Database/LegacyDatabase/SUKLegacy.swift | 106 +++ .../_001_InitialSetupMigration.swift | 3 +- .../Migrations/_002_SetupStandardJobs.swift | 3 +- .../Migrations/_003_YDBToGRDBMigration.swift | 21 +- .../Database/Types/Migration.swift | 11 + .../Types/PagedDatabaseObserver.swift | 2 +- .../Database/Types/TargetMigrations.swift | 6 +- .../DatabaseMigrator+Utilities.swift | 7 +- .../General/NSArray+Functional.h | 9 - .../General/NSArray+Functional.m | 32 - SessionUtilitiesKit/JobRunner/JobRunner.swift | 213 ++++-- .../Meta/SessionUtilitiesKit.h | 1 - .../BlockingManagerRemovalMigration.swift | 62 -- .../Migrations/ContactsMigration.swift | 57 -- .../Migrations/MessageRequestsMigration.swift | 93 --- .../Migrations/OWSDatabaseMigration.h | 25 - .../Migrations/OWSDatabaseMigration.m | 109 --- .../Migrations/OWSDatabaseMigrationRunner.h | 23 - .../Migrations/OWSDatabaseMigrationRunner.m | 124 ---- .../OWSResaveCollectionDBMigration.h | 24 - .../OWSResaveCollectionDBMigration.m | 80 --- .../OpenGroupServerIdLookupMigration.swift | 71 -- .../AttachmentApprovalViewController.swift | 8 +- .../AttachmentItemCollection.swift | 18 +- SignalUtilitiesKit/Meta/SignalUtilitiesKit.h | 8 - .../Shared Views/GalleryRailView.swift | 107 ++- SignalUtilitiesKit/Utilities/NSArray+OWS.h | 15 - SignalUtilitiesKit/Utilities/NSArray+OWS.m | 25 - .../Utilities/NSObject+Casting.h | 7 - .../Utilities/NSObject+Casting.m | 10 - .../Utilities/NSSet+Functional.h | 9 - .../Utilities/NSSet+Functional.m | 32 - .../Utilities/OWSAnyTouchGestureRecognizer.h | 22 - .../Utilities/OWSAnyTouchGestureRecognizer.m | 132 ---- .../Utilities/UIGestureRecognizer+OWS.swift | 17 +- 90 files changed, 1193 insertions(+), 1636 deletions(-) delete mode 100644 SessionUtilitiesKit/General/NSArray+Functional.h delete mode 100644 SessionUtilitiesKit/General/NSArray+Functional.m delete mode 100644 SignalUtilitiesKit/Database/Migrations/BlockingManagerRemovalMigration.swift delete mode 100644 SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift delete mode 100644 SignalUtilitiesKit/Database/Migrations/MessageRequestsMigration.swift delete mode 100644 SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.h delete mode 100644 SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.m delete mode 100644 SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.h delete mode 100644 SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m delete mode 100644 SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.h delete mode 100644 SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.m delete mode 100644 SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift delete mode 100644 SignalUtilitiesKit/Utilities/NSArray+OWS.h delete mode 100644 SignalUtilitiesKit/Utilities/NSArray+OWS.m delete mode 100644 SignalUtilitiesKit/Utilities/NSObject+Casting.h delete mode 100644 SignalUtilitiesKit/Utilities/NSObject+Casting.m delete mode 100644 SignalUtilitiesKit/Utilities/NSSet+Functional.h delete mode 100644 SignalUtilitiesKit/Utilities/NSSet+Functional.m delete mode 100644 SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.h delete mode 100644 SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.m diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 853ff8d42..ef125b14f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -207,7 +207,6 @@ B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; }; B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; }; B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF4BB326A5204600583500 /* SendSeedModal.swift */; }; - B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; }; B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; }; B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; }; B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.swift */; }; @@ -297,8 +296,6 @@ C32C5EE5256DF506003C73A2 /* YapDatabaseConnection+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB5F255A580E00E217F9 /* YapDatabaseConnection+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; C32C5EEE256DF54E003C73A2 /* TSDatabaseView.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB46255A580C00E217F9 /* TSDatabaseView.m */; }; C32C5EF7256DF567003C73A2 /* TSDatabaseView.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB2C255A580A00E217F9 /* TSDatabaseView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB8255A580100E217F9 /* NSArray+Functional.m */; }; - C32C5FAA256DFED9003C73A2 /* NSArray+Functional.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */; settings = {ATTRIBUTES = (Public, ); }; }; C32C5FBB256E0206003C73A2 /* OWSBackgroundTask.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */; }; C32C5FC4256E0209003C73A2 /* OWSBackgroundTask.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */; }; @@ -340,14 +337,10 @@ C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA96255A57FE00E217F9 /* OWSDispatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDC53255A582000E217F9 /* OutageDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA99255A57FE00E217F9 /* OutageDetection.swift */; }; C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; }; - C33FDC64255A582000E217F9 /* NSObject+Casting.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAAA255A580000E217F9 /* NSObject+Casting.m */; }; C33FDC78255A582000E217F9 /* TSConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDABE255A580100E217F9 /* TSConstants.m */; }; - C33FDC7B255A582000E217F9 /* NSSet+Functional.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC1255A580100E217F9 /* NSSet+Functional.m */; }; C33FDC7D255A582000E217F9 /* OWSDispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC3255A580200E217F9 /* OWSDispatch.m */; }; - C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDADC255A580400E217F9 /* NSObject+Casting.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADE255A580400E217F9 /* SwiftSingletons.swift */; }; C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE0255A580400E217F9 /* ByteParser.m */; }; - C33FDCC7255A582000E217F9 /* NSArray+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB0D255A580800E217F9 /* NSArray+OWS.m */; }; C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB17255A580800E217F9 /* FunctionalUtil.m */; }; C33FDCFA255A582000E217F9 /* SignalIOSProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */; }; C33FDD03255A582000E217F9 /* WeakTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB49255A580C00E217F9 /* WeakTimer.swift */; }; @@ -362,9 +355,7 @@ C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; }; C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */; }; C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDDB3255A582000E217F9 /* OWSError.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF9255A581C00E217F9 /* OWSError.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBFE255A581C00E217F9 /* NSSet+Functional.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC03255A581D00E217F9 /* ByteParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDDC5255A582000E217F9 /* OWSError.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC0B255A581D00E217F9 /* OWSError.m */; }; C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC12255A581E00E217F9 /* TSConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -396,7 +387,6 @@ C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */; }; C374EEF425DB31D40073A857 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C374EEF325DB31D40073A857 /* VoiceMessageRecordingView.swift */; }; C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */; }; - C37F5396255B95BD002AEA92 /* OWSAnyTouchGestureRecognizer.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF302255B6DBE007E1867 /* OWSAnyTouchGestureRecognizer.h */; settings = {ATTRIBUTES = (Public, ); }; }; C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */; }; @@ -413,19 +403,12 @@ C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; }; C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; }; C38EF24F255B6D67007E1867 /* UIColor+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF242255B6D67007E1867 /* UIColor+OWS.m */; }; - C38EF272255B6D7A007E1867 /* OWSResaveCollectionDBMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF26C255B6D79007E1867 /* OWSResaveCollectionDBMigration.m */; }; - C38EF273255B6D7A007E1867 /* OWSDatabaseMigrationRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF26D255B6D79007E1867 /* OWSDatabaseMigrationRunner.m */; }; - C38EF274255B6D7A007E1867 /* OWSResaveCollectionDBMigration.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF26E255B6D79007E1867 /* OWSResaveCollectionDBMigration.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C38EF275255B6D7A007E1867 /* OWSDatabaseMigrationRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C38EF276255B6D7A007E1867 /* OWSDatabaseMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */; }; - C38EF277255B6D7A007E1867 /* OWSDatabaseMigration.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */; settings = {ATTRIBUTES = (Public, ); }; }; C38EF2A5255B6D93007E1867 /* Identicon+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A2255B6D93007E1867 /* Identicon+ObjC.swift */; }; C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */; }; C38EF2A7255B6D93007E1867 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */; }; C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */; }; C38EF2B4255B6D9C007E1867 /* UIView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B2255B6D9C007E1867 /* UIView+Utilities.swift */; }; C38EF30C255B6DBF007E1867 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* OWSScreenLock.swift */; }; - C38EF31A255B6DBF007E1867 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F0255B6DBB007E1867 /* OWSAnyTouchGestureRecognizer.m */; }; C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F2255B6DBC007E1867 /* Searcher.swift */; }; C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F3255B6DBC007E1867 /* UIImage+OWS.swift */; }; C38EF324255B6DBF007E1867 /* Bench.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2FA255B6DBD007E1867 /* Bench.swift */; }; @@ -650,10 +633,8 @@ FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; - FD28A4F227E990E800FF65E7 /* BlockingManagerRemovalMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */; }; FD28A4F427EA79F800FF65E7 /* BlockListUIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; - FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */; }; FD3C907527E83AC200CD579F /* OpenGroupServerIdLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */; }; FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; }; @@ -680,7 +661,6 @@ FD848B9C284435D7000E298B /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9B284435D7000E298B /* AppSetup.swift */; }; FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; }; FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; }; - FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; }; FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; }; FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; }; FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; }; @@ -1139,7 +1119,6 @@ B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = ""; }; B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = ""; }; - B8B32044258C117C0020074B /* ContactsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsMigration.swift; sourceTree = ""; }; B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = ""; }; B8B5BCEB2394D869003823C9 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; B8BAC75B2695645400EA1759 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; @@ -1226,14 +1205,10 @@ C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseDispatchQueue.swift; sourceTree = ""; }; C33FDAA1255A57FF00E217F9 /* TSYapDatabaseObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSYapDatabaseObject.h; sourceTree = ""; }; C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; - C33FDAAA255A580000E217F9 /* NSObject+Casting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+Casting.m"; sourceTree = ""; }; C33FDAB1255A580000E217F9 /* OWSStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSStorage.m; sourceTree = ""; }; - C33FDAB8255A580100E217F9 /* NSArray+Functional.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Functional.m"; sourceTree = ""; }; C33FDAB9255A580100E217F9 /* OWSStorage+Subclass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSStorage+Subclass.h"; sourceTree = ""; }; C33FDABE255A580100E217F9 /* TSConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSConstants.m; sourceTree = ""; }; - C33FDAC1255A580100E217F9 /* NSSet+Functional.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSSet+Functional.m"; sourceTree = ""; }; C33FDAC3255A580200E217F9 /* OWSDispatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDispatch.m; sourceTree = ""; }; - C33FDADC255A580400E217F9 /* NSObject+Casting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+Casting.h"; sourceTree = ""; }; C33FDADE255A580400E217F9 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = ""; }; C33FDAE0255A580400E217F9 /* ByteParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ByteParser.m; sourceTree = ""; }; C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupFragment.h; sourceTree = ""; }; @@ -1245,7 +1220,6 @@ C33FDAFE255A580600E217F9 /* OWSStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSStorage.h; sourceTree = ""; }; C33FDB01255A580700E217F9 /* AppReadiness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppReadiness.h; sourceTree = ""; }; C33FDB07255A580700E217F9 /* OWSBackupFragment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupFragment.m; sourceTree = ""; }; - C33FDB0D255A580800E217F9 /* NSArray+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+OWS.m"; sourceTree = ""; }; C33FDB12255A580800E217F9 /* NSString+SSK.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+SSK.h"; sourceTree = ""; }; C33FDB14255A580800E217F9 /* OWSMath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMath.h; sourceTree = ""; }; C33FDB17255A580800E217F9 /* FunctionalUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FunctionalUtil.m; sourceTree = ""; }; @@ -1271,7 +1245,6 @@ C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSUserDefaults+OWS.h"; sourceTree = ""; }; C33FDB54255A580D00E217F9 /* DataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataSource.h; sourceTree = ""; }; C33FDB5B255A580E00E217F9 /* YapDatabaseTransaction+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "YapDatabaseTransaction+OWS.m"; sourceTree = ""; }; - C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Functional.h"; sourceTree = ""; }; C33FDB5F255A580E00E217F9 /* YapDatabaseConnection+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YapDatabaseConnection+OWS.h"; sourceTree = ""; }; C33FDB68255A580F00E217F9 /* ContentProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentProxy.swift; sourceTree = ""; }; C33FDB69255A580F00E217F9 /* FeatureFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; @@ -1298,9 +1271,7 @@ C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationAPI.swift; sourceTree = ""; }; C33FDBE1255A581A00E217F9 /* LKGroupUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKGroupUtilities.m; sourceTree = ""; }; C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLSessionDataTask+StatusCode.h"; sourceTree = ""; }; - C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+OWS.h"; sourceTree = ""; }; C33FDBF9255A581C00E217F9 /* OWSError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSError.h; sourceTree = ""; }; - C33FDBFE255A581C00E217F9 /* NSSet+Functional.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSSet+Functional.h"; sourceTree = ""; }; C33FDC02255A581D00E217F9 /* OWSPrimaryStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSPrimaryStorage.m; sourceTree = ""; }; C33FDC03255A581D00E217F9 /* ByteParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ByteParser.h; sourceTree = ""; }; C33FDC0B255A581D00E217F9 /* OWSError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSError.m; sourceTree = ""; }; @@ -1351,12 +1322,6 @@ C38EF240255B6D67007E1867 /* UIView+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+OWS.swift"; path = "SignalUtilitiesKit/Utilities/UIView+OWS.swift"; sourceTree = SOURCE_ROOT; }; C38EF241255B6D67007E1867 /* Collection+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Collection+OWS.swift"; path = "SignalUtilitiesKit/Utilities/Collection+OWS.swift"; sourceTree = SOURCE_ROOT; }; C38EF242255B6D67007E1867 /* UIColor+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIColor+OWS.m"; path = "SignalUtilitiesKit/Utilities/UIColor+OWS.m"; sourceTree = SOURCE_ROOT; }; - C38EF26C255B6D79007E1867 /* OWSResaveCollectionDBMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSResaveCollectionDBMigration.m; path = SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.m; sourceTree = SOURCE_ROOT; }; - C38EF26D255B6D79007E1867 /* OWSDatabaseMigrationRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDatabaseMigrationRunner.m; path = SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m; sourceTree = SOURCE_ROOT; }; - C38EF26E255B6D79007E1867 /* OWSResaveCollectionDBMigration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSResaveCollectionDBMigration.h; path = SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.h; sourceTree = SOURCE_ROOT; }; - C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSDatabaseMigrationRunner.h; path = SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.h; sourceTree = SOURCE_ROOT; }; - C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDatabaseMigration.m; path = SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.m; sourceTree = SOURCE_ROOT; }; - C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSDatabaseMigration.h; path = SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.h; sourceTree = SOURCE_ROOT; }; C38EF281255B6D84007E1867 /* OWSAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSAudioSession.swift; path = SessionMessagingKit/Utilities/OWSAudioSession.swift; sourceTree = SOURCE_ROOT; }; C38EF2A2255B6D93007E1867 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Identicon+ObjC.swift"; path = "SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift"; sourceTree = SOURCE_ROOT; }; C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlaceholderIcon.swift; path = "SignalUtilitiesKit/Profile Pictures/PlaceholderIcon.swift"; sourceTree = SOURCE_ROOT; }; @@ -1366,7 +1331,6 @@ C38EF2E2255B6DB9007E1867 /* OWSScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSScreenLock.swift; path = "SignalUtilitiesKit/Screen Lock/OWSScreenLock.swift"; sourceTree = SOURCE_ROOT; }; C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; }; C38EF2EF255B6DBB007E1867 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SessionUtilitiesKit/General/Weak.swift; sourceTree = SOURCE_ROOT; }; - C38EF2F0255B6DBB007E1867 /* OWSAnyTouchGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSAnyTouchGestureRecognizer.m; path = SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.m; sourceTree = SOURCE_ROOT; }; C38EF2F1255B6DBB007E1867 /* OWSPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSPreferences.h; path = SessionMessagingKit/Utilities/OWSPreferences.h; sourceTree = SOURCE_ROOT; }; C38EF2F2255B6DBC007E1867 /* Searcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Searcher.swift; path = SignalUtilitiesKit/Utilities/Searcher.swift; sourceTree = SOURCE_ROOT; }; C38EF2F3255B6DBC007E1867 /* UIImage+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIImage+OWS.swift"; path = "SignalUtilitiesKit/Utilities/UIImage+OWS.swift"; sourceTree = SOURCE_ROOT; }; @@ -1376,7 +1340,6 @@ C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSWindowManager.h; path = SessionMessagingKit/Utilities/OWSWindowManager.h; sourceTree = SOURCE_ROOT; }; C38EF300255B6DBD007E1867 /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UIUtil.m; path = SignalUtilitiesKit/Utilities/UIUtil.m; sourceTree = SOURCE_ROOT; }; C38EF301255B6DBD007E1867 /* OWSFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSFormat.h; path = SignalUtilitiesKit/Utilities/OWSFormat.h; sourceTree = SOURCE_ROOT; }; - C38EF302255B6DBE007E1867 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.h; sourceTree = SOURCE_ROOT; }; C38EF304255B6DBE007E1867 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = SignalUtilitiesKit/Utilities/ImageCache.swift; sourceTree = SOURCE_ROOT; }; C38EF305255B6DBE007E1867 /* OWSFormat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSFormat.m; path = SignalUtilitiesKit/Utilities/OWSFormat.m; sourceTree = SOURCE_ROOT; }; C38EF306255B6DBE007E1867 /* OWSWindowManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSWindowManager.m; path = SessionMessagingKit/Utilities/OWSWindowManager.m; sourceTree = SOURCE_ROOT; }; @@ -1611,11 +1574,9 @@ FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUKLegacy.swift; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; - FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockingManagerRemovalMigration.swift; sourceTree = ""; }; FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = ""; }; FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBStorage.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; - FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = ""; }; FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = ""; }; @@ -1642,7 +1603,6 @@ FD848B9B284435D7000E298B /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = ""; }; FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = ""; }; FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; - FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = ""; }; FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; @@ -2203,8 +2163,6 @@ B8BC00BF257D90E30032E807 /* General.swift */, C3C2A5CE2553860700C340D1 /* Logging.swift */, C33FDAFD255A580600E217F9 /* LRUCache.swift */, - C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */, - C33FDAB8255A580100E217F9 /* NSArray+Functional.m */, C33FDB3B255A580B00E217F9 /* NSNotificationCenter+OWS.h */, C33FDB6C255A580F00E217F9 /* NSNotificationCenter+OWS.m */, C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */, @@ -2749,16 +2707,6 @@ C379DCE82567330E0002D4EB /* Migrations */ = { isa = PBXGroup; children = ( - B8B32044258C117C0020074B /* ContactsMigration.swift */, - FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */, - FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */, - FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */, - C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */, - C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */, - C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */, - C38EF26D255B6D79007E1867 /* OWSDatabaseMigrationRunner.m */, - C38EF26E255B6D79007E1867 /* OWSResaveCollectionDBMigration.h */, - C38EF26C255B6D79007E1867 /* OWSResaveCollectionDBMigration.m */, ); path = Migrations; sourceTree = ""; @@ -3024,8 +2972,6 @@ isa = PBXGroup; children = ( FD848B9B284435D7000E298B /* AppSetup.swift */, - C38EF302255B6DBE007E1867 /* OWSAnyTouchGestureRecognizer.h */, - C38EF2F0255B6DBB007E1867 /* OWSAnyTouchGestureRecognizer.m */, FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */, C38EF3DC255B6DF1007E1867 /* DirectionalPanGestureRecognizer.swift */, C38EF240255B6D67007E1867 /* UIView+OWS.swift */, @@ -3072,14 +3018,8 @@ C33FDB49255A580C00E217F9 /* WeakTimer.swift */, C33FDBC2255A581700E217F9 /* SSKAsserts.h */, C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */, - C33FDADC255A580400E217F9 /* NSObject+Casting.h */, - C33FDAAA255A580000E217F9 /* NSObject+Casting.m */, - C33FDBFE255A581C00E217F9 /* NSSet+Functional.h */, - C33FDAC1255A580100E217F9 /* NSSet+Functional.m */, C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */, C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */, - C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */, - C33FDB0D255A580800E217F9 /* NSArray+OWS.m */, C33FDC03255A581D00E217F9 /* ByteParser.h */, C33FDAE0255A580400E217F9 /* ByteParser.m */, C38EF3DD255B6DF1007E1867 /* UIAlertController+OWS.swift */, @@ -3527,23 +3467,16 @@ C38EF3F6255B6DF7007E1867 /* OWSTextView.h in Headers */, C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */, C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */, - C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */, C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */, C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */, C38EF243255B6D67007E1867 /* UIViewController+OWS.h in Headers */, C38EF35D255B6DCC007E1867 /* OWSNavigationController.h in Headers */, C38EF249255B6D67007E1867 /* UIColor+OWS.h in Headers */, - C38EF274255B6D7A007E1867 /* OWSResaveCollectionDBMigration.h in Headers */, - C38EF277255B6D7A007E1867 /* OWSDatabaseMigration.h in Headers */, C38EF3F5255B6DF7007E1867 /* OWSTextField.h in Headers */, - C38EF275255B6D7A007E1867 /* OWSDatabaseMigrationRunner.h in Headers */, C38EF366255B6DCC007E1867 /* ScreenLockViewController.h in Headers */, C33FDDD3255A582000E217F9 /* OWSQueues.h in Headers */, - C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */, C33FDDB3255A582000E217F9 /* OWSError.h in Headers */, C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */, - C37F5396255B95BD002AEA92 /* OWSAnyTouchGestureRecognizer.h in Headers */, - C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */, C38EF367255B6DCC007E1867 /* OWSTableViewController.h in Headers */, C38EF246255B6D67007E1867 /* UIFont+OWS.h in Headers */, C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */, @@ -3564,7 +3497,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C32C5FAA256DFED9003C73A2 /* NSArray+Functional.h in Headers */, C3D9E3FA25676BCE0040E4F3 /* TSYapDatabaseObject.h in Headers */, C3D9E3A4256763DE0040E4F3 /* AppContext.h in Headers */, C3D9E38A256760390040E4F3 /* OWSFileSystem.h in Headers */, @@ -4286,14 +4218,11 @@ C38EF3FD255B6DF7007E1867 /* OWSTextView.m in Sources */, C38EF3C6255B6DE7007E1867 /* ImageEditorModel.swift in Sources */, C38EF3C3255B6DE7007E1867 /* ImageEditorTextItem.swift in Sources */, - FD28A4F227E990E800FF65E7 /* BlockingManagerRemovalMigration.swift in Sources */, C33FDC7D255A582000E217F9 /* OWSDispatch.m in Sources */, C38EF247255B6D67007E1867 /* NSAttributedString+OWS.m in Sources */, C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */, C38EF3C5255B6DE7007E1867 /* OWSViewController+ImageEditor.swift in Sources */, C38EF2A5255B6D93007E1867 /* Identicon+ObjC.swift in Sources */, - C38EF273255B6D7A007E1867 /* OWSDatabaseMigrationRunner.m in Sources */, - C38EF31A255B6DBF007E1867 /* OWSAnyTouchGestureRecognizer.m in Sources */, C38EF385255B6DD2007E1867 /* AttachmentTextToolbar.swift in Sources */, C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */, C38EF389255B6DD2007E1867 /* AttachmentTextView.swift in Sources */, @@ -4311,19 +4240,16 @@ C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */, C38EF402255B6DF7007E1867 /* CommonStrings.swift in Sources */, C38EF3C1255B6DE7007E1867 /* ImageEditorBrushViewController.swift in Sources */, - C33FDCC7255A582000E217F9 /* NSArray+OWS.m in Sources */, C33FDD32255A582000E217F9 /* OWSOperation.m in Sources */, C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */, C3D90A7A25773A93002C9DF5 /* Configuration.swift in Sources */, C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */, - C33FDC64255A582000E217F9 /* NSObject+Casting.m in Sources */, C38EF388255B6DD2007E1867 /* AttachmentApprovalViewController.swift in Sources */, C38EF2A7255B6D93007E1867 /* ProfilePictureView.swift in Sources */, C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */, C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */, C38EF407255B6DF7007E1867 /* Toast.swift in Sources */, C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */, - FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */, C38EF32A255B6DBF007E1867 /* UIUtil.m in Sources */, C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */, C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */, @@ -4335,16 +4261,13 @@ C38EF3BA255B6DE7007E1867 /* ImageEditorItem.swift in Sources */, C38EF3F7255B6DF7007E1867 /* OWSNavigationBar.swift in Sources */, C38EF248255B6D67007E1867 /* UIViewController+OWS.m in Sources */, - C38EF272255B6D7A007E1867 /* OWSResaveCollectionDBMigration.m in Sources */, C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */, - C38EF276255B6D7A007E1867 /* OWSDatabaseMigration.m in Sources */, C38EF370255B6DCC007E1867 /* OWSNavigationController.m in Sources */, C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */, C33FDCFA255A582000E217F9 /* SignalIOSProto.swift in Sources */, C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */, C38EF38B255B6DD2007E1867 /* AttachmentPrepViewController.swift in Sources */, - C33FDC7B255A582000E217F9 /* NSSet+Functional.m in Sources */, C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */, C38EF3C4255B6DE7007E1867 /* ImageEditorContents.swift in Sources */, C38EF3BC255B6DE7007E1867 /* ImageEditorPanGestureRecognizer.swift in Sources */, @@ -4370,11 +4293,9 @@ FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */, C38EF3F9255B6DF7007E1867 /* OWSLayerView.swift in Sources */, C33FDD03255A582000E217F9 /* WeakTimer.swift in Sources */, - B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */, C38EF3B9255B6DE7007E1867 /* ImageEditorPinchGestureRecognizer.swift in Sources */, C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */, C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */, - FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */, C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */, C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */, C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */, @@ -4460,7 +4381,6 @@ FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */, - C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */, C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */, FD17D7BD27F51F6900122BE0 /* GRDB+Notifications.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index c261436f7..dfdb0b215 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1325,6 +1325,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers } func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Int64]?, searchText: String?) { + viewModel.lastSearchedText = searchText tableView.reloadRows(at: tableView.indexPathsForVisibleRows ?? [], with: UITableView.RowAnimation.none) } diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index d25a8f966..8c7d2ea0d 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -65,10 +65,6 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { columns: Interaction.Columns .allCases .filter { $0 != .wasRead } - ), - PagedData.ObservedChanges( - table: ThreadTypingIndicator.self, - columns: ThreadTypingIndicator.Columns.allCases ) ], filterSQL: MessageViewModel.filterSQL(threadId: threadId), @@ -90,6 +86,19 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { joinToPagedType: MessageViewModel.AttachmentInteractionInfo.joinToViewModelQuerySQL, groupPagedType: MessageViewModel.AttachmentInteractionInfo.groupViewModelQuerySQL, associateData: MessageViewModel.AttachmentInteractionInfo.createAssociateDataClosure() + ), + AssociatedRecord( + trackedAgainst: ThreadTypingIndicator.self, + observedChanges: [ + PagedData.ObservedChanges( + table: ThreadTypingIndicator.self, + events: [.insert, .delete], + columns: [] + ) + ], + dataQuery: MessageViewModel.TypingIndicatorInfo.baseQuery, + joinToPagedType: MessageViewModel.TypingIndicatorInfo.joinToViewModelQuerySQL, + associateData: MessageViewModel.TypingIndicatorInfo.createAssociateDataClosure() ) ], onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in @@ -140,6 +149,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { /// This value is the current state of the view public private(set) var threadData: SessionThreadViewModel + /// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise + /// performance https://github.com/groue/GRDB.swift#valueobservation-performance + /// + /// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static + /// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries + /// + /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) + /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own + /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) + /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this public lazy var observableThreadData = ValueObservation .trackingConstantRegion { [threadId = self.threadId] db -> SessionThreadViewModel? in let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -161,7 +180,9 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { public var onInteractionChange: (([SectionModel]) -> ())? private func process(data: [MessageViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] { + let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator }) let sortedData: [MessageViewModel] = data + .filter { !$0.isTypingIndicator } .sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs } // We load messages from newest to oldest so having a pageOffset larger than zero means @@ -186,6 +207,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { ) ) } + .appending(typingIndicator) ) ], (!data.isEmpty && pageInfo.pageOffset > 0 ? diff --git a/Session/Conversations/Settings/OWSConversationSettingsViewController.m b/Session/Conversations/Settings/OWSConversationSettingsViewController.m index d53be144d..8a0db3674 100644 --- a/Session/Conversations/Settings/OWSConversationSettingsViewController.m +++ b/Session/Conversations/Settings/OWSConversationSettingsViewController.m @@ -9,7 +9,6 @@ #import "UIView+OWS.h" #import #import -#import #import #import #import diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 74eee689b..7cd8d113b 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -37,6 +37,17 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve return result }() + + private lazy var loadingConversationsLabel: UILabel = { + let result: UILabel = UILabel() + result.font = UIFont.systemFont(ofSize: Values.smallFontSize) + result.text = "LOADING_CONVERSATIONS".localized() + result.textColor = Colors.text + result.textAlignment = .center + result.numberOfLines = 0 + + return result + }() private lazy var tableView: UITableView = { let result = UITableView() @@ -128,6 +139,13 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve seedReminderView.pin(.trailing, to: .trailing, of: view) } + // Loading conversations label + view.addSubview(loadingConversationsLabel) + + loadingConversationsLabel.pin(.top, to: .top, of: view, withInset: Values.veryLargeSpacing) + loadingConversationsLabel.pin(.leading, to: .leading, of: view, withInset: 50) + loadingConversationsLabel.pin(.trailing, to: .trailing, of: view, withInset: -50) + // Table view view.addSubview(tableView) tableView.pin(.leading, to: .leading, of: view) @@ -218,11 +236,9 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve // Start observing for data changes dataChangeObservable = GRDBStorage.shared.start( viewModel.observableViewData, - onError: { error in - print("Update error!!!!") - }, + onError: { _ in }, onChange: { [weak self] viewData in - // The defaul scheduler emits changes on the main thread + // The default scheduler emits changes on the main thread self?.handleUpdates(viewData) } ) @@ -237,6 +253,9 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve return } + // Hide the 'loading conversations' label (now that we have received conversation data) + loadingConversationsLabel.isHidden = true + // Show the empty state if there is no data emptyStateView.isHidden = ( !updatedViewData.isEmpty && diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index e309e8076..a17730a42 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -17,43 +17,68 @@ public class HomeViewModel { /// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise /// performance https://github.com/groue/GRDB.swift#valueobservation-performance /// - /// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static - /// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries + /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) + /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own + /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) + /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this public lazy var observableViewData = ValueObservation - .trackingConstantRegion { db -> [ArraySection] in - let userPublicKey: String = getUserHexEncodedPublicKey(db) - let unreadMessageRequestCount: Int = try SessionThread - .filter(SessionThread.isMessageRequest(userPublicKey: userPublicKey)) - .joining(optional: SessionThread.contact) - .joining( - required: SessionThread.interactions - .filter(Interaction.Columns.wasRead == false) - ) - .group(SessionThread.Columns.id) - .fetchCount(db) - let finalUnreadMessageRequestCount: Int = (db[.hasHiddenMessageRequests] ? 0 : unreadMessageRequestCount) - - return [ - ArraySection( - model: .messageRequests, - elements: [ - // If there are no unread message requests then hide the message request banner - (finalUnreadMessageRequestCount == 0 ? - nil : - SessionThreadViewModel( - unreadCount: UInt(finalUnreadMessageRequestCount) - ) - ) - ].compactMap { $0 } + .tracking( + regions: [ + // We explicitly define the regions we want to track as the automatic detection + // seems to include a bunch of columns we will fetch but probably don't need to + // track changes for + SessionThread.select( + .id, + .shouldBeVisible, + .isPinned, + .mutedUntilTimestamp, + .onlyNotifyForMentions + ), + Setting.filter(id: Setting.BoolKey.hasHiddenMessageRequests.rawValue), + Contact.select(.isBlocked, .isApproved), // 'isApproved' for message requests + Profile.select(.name, .nickname, .profilePictureFileName), + ClosedGroup.select(.name), + OpenGroup.select(.name, .imageData), + GroupMember.select(.groupId), + Interaction.select( + .body, + .wasRead ), - ArraySection( - model: .threads, - elements: try SessionThreadViewModel - .homeQuery(userPublicKey: userPublicKey) - .fetchAll(db) - ) - ] - } + Attachment.select(.state), + RecipientState.select(.state), + ThreadTypingIndicator.select(.threadId) + ], + fetch: { db -> [ArraySection] in + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let unreadMessageRequestCount: Int = try SessionThread + .unreadMessageRequestsCountQuery(userPublicKey: userPublicKey) + .fetchOne(db) + .defaulting(to: 0) + let finalUnreadMessageRequestCount: Int = (db[.hasHiddenMessageRequests] ? 0 : unreadMessageRequestCount) + let threads: [SessionThreadViewModel] = try SessionThreadViewModel + .homeQuery(userPublicKey: userPublicKey) + .fetchAll(db) + + return [ + ArraySection( + model: .messageRequests, + elements: [ + // If there are no unread message requests then hide the message request banner + (finalUnreadMessageRequestCount == 0 ? + nil : + SessionThreadViewModel( + unreadCount: UInt(finalUnreadMessageRequestCount) + ) + ) + ].compactMap { $0 } + ), + ArraySection( + model: .threads, + elements: threads + ) + ] + } + ) .removeDuplicates() // MARK: - Functions diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index 9688d1c71..d1ce5633d 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -14,6 +14,11 @@ public class MessageRequestsViewModel { /// /// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static /// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries + /// + /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) + /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own + /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) + /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this public lazy var observableViewData = ValueObservation .trackingConstantRegion { db -> [SessionThreadViewModel] in let userPublicKey: String = getUserHexEncodedPublicKey(db) diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index 45d316bc1..f61ea68b6 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -5,6 +5,7 @@ import Foundation import Photos import PromiseKit +import SessionUIKit protocol ImagePickerGridControllerDelegate: AnyObject { func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) @@ -46,6 +47,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat override func viewDidLoad() { super.viewDidLoad() + + self.view.backgroundColor = Colors.navigationBarBackground library.add(delegate: self) @@ -59,7 +62,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // ensure images at the end of the list can be scrolled above the bottom buttons let bottomButtonInset = -1 * SendMediaNavigationController.bottomButtonsCenterOffset + SendMediaNavigationController.bottomButtonWidth / 2 + 16 collectionView.contentInset.bottom = bottomButtonInset + 16 - view.backgroundColor = .white // The PhotoCaptureVC needs a shadow behind it's cancel button, so we use a custom icon. // This VC has a visible navbar so doesn't need the shadow, but because the user can @@ -69,7 +71,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let cancelImage = UIImage(imageLiteralResourceName: "X") let cancelButton = UIBarButtonItem(image: cancelImage, style: .plain, target: self, action: #selector(didPressCancel)) - cancelButton.tintColor = .black + cancelButton.tintColor = Colors.text navigationItem.leftBarButtonItem = cancelButton let titleView = TitleView() @@ -86,7 +88,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat navigationItem.titleView = titleView self.titleView = titleView - collectionView.backgroundColor = .white + collectionView.backgroundColor = Colors.navigationBarBackground let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection)) selectionPanGesture.delegate = self @@ -187,16 +189,15 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - // Loki: Set navigation bar background color - let navigationBar = navigationController!.navigationBar - navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) - navigationBar.shadowImage = UIImage() - navigationBar.isTranslucent = false - navigationBar.barTintColor = .white - (navigationBar as! OWSNavigationBar).respectsTheme = false - navigationBar.backgroundColor = .white - let backgroundImage = UIImage(color: .white) - navigationBar.setBackgroundImage(backgroundImage, for: .default) + let backgroundImage: UIImage = UIImage(color: Colors.navigationBarBackground) + self.navigationItem.title = nil + self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + self.navigationController?.navigationBar.shadowImage = UIImage() + self.navigationController?.navigationBar.isTranslucent = false + self.navigationController?.navigationBar.barTintColor = Colors.navigationBarBackground + (self.navigationController?.navigationBar as? OWSNavigationBar)?.respectsTheme = true + self.navigationController?.navigationBar.backgroundColor = Colors.navigationBarBackground + self.navigationController?.navigationBar.setBackgroundImage(backgroundImage, for: .default) // Determine the size of the thumbnails to request let scale = UIScreen.main.scale @@ -605,10 +606,10 @@ class TitleView: UIView { addSubview(stackView) stackView.autoPinEdgesToSuperviewEdges() - label.textColor = .black + label.textColor = Colors.text label.font = .boldSystemFont(ofSize: Values.mediumFontSize) - iconView.tintColor = .black + iconView.tintColor = Colors.text iconView.image = UIImage(named: "navbar_disclosure_down")?.withRenderingMode(.alwaysTemplate) addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(titleTapped))) diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 18f41c8a1..22f7249f6 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -309,6 +309,11 @@ public class MediaGalleryViewModel { /// /// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static /// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries + /// + /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) + /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own + /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) + /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this public typealias AlbumObservation = ValueObservation>> public lazy var observableAlbumData: AlbumObservation = buildAlbumObservation(for: nil) diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift index e0211fab3..6af3f3090 100644 --- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift +++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift @@ -282,8 +282,6 @@ extension SendMediaNavigationController: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { if viewController == captureViewController { setNavBarBackgroundColor(to: .black) - } else if viewController == mediaLibraryViewController { - setNavBarBackgroundColor(to: .white) } else { setNavBarBackgroundColor(to: Colors.navigationBarBackground) } @@ -311,8 +309,6 @@ extension SendMediaNavigationController: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { if viewController == captureViewController { setNavBarBackgroundColor(to: .black) - } else if viewController == mediaLibraryViewController { - setNavBarBackgroundColor(to: .white) } else { setNavBarBackgroundColor(to: Colors.navigationBarBackground) } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 2b2ad8a6f..846daa04e 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -18,9 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var hasInitialRootViewController: Bool = false /// This needs to be a lazy variable to ensure it doesn't get initialized before it actually needs to be used - lazy var poller: Poller = { - return Poller() - }() + lazy var poller: Poller = Poller() // MARK: - Lifecycle @@ -67,6 +65,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD migrationsCompletion: { [weak self] successful, needsConfigSync in guard let strongSelf = self else { return } guard successful else { + self?.showFailedMigrationAlert() return } @@ -211,6 +210,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - App Readiness + private func showFailedMigrationAlert() { + let alert = UIAlertController( + title: "Session", + message: [ + "DATABASE_MIGRATION_FAILED".localized(), + "modal_share_logs_explanation".localized() + ].joined(separator: "\n\n"), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "modal_share_logs_title".localized(), style: .default) { _ in + ShareLogsModal.shareLogs(from: alert) { [weak self] in + self?.showFailedMigrationAlert() + } + }) + alert.addAction(UIAlertAction(title: "Close", style: .destructive) { _ in + DDLog.flushLog() + exit(0) + }) + + self.window?.rootViewController?.present(alert, animated: true, completion: nil) + } + /// The user must unlock the device once after reboot before the database encryption key can be accessed. private func verifyDBKeysAvailableBeforeBackgroundLaunch() { guard UIApplication.shared.applicationState == .background else { return } diff --git a/Session/Meta/MainAppContext.m b/Session/Meta/MainAppContext.m index 1a1cd6039..bfcf801a2 100644 --- a/Session/Meta/MainAppContext.m +++ b/Session/Meta/MainAppContext.m @@ -5,7 +5,6 @@ #import "MainAppContext.h" #import "Session-Swift.h" #import -#import #import NS_ASSUME_NONNULL_BEGIN diff --git a/Session/Meta/Session-Prefix.pch b/Session/Meta/Session-Prefix.pch index 5dbd16ebf..8998c4792 100644 --- a/Session/Meta/Session-Prefix.pch +++ b/Session/Meta/Session-Prefix.pch @@ -11,8 +11,5 @@ #import #import #import - #import - #import - #import #import #endif diff --git a/Session/Meta/Signal-Bridging-Header.h b/Session/Meta/Signal-Bridging-Header.h index 01ad3a46c..bee17ac82 100644 --- a/Session/Meta/Signal-Bridging-Header.h +++ b/Session/Meta/Signal-Bridging-Header.h @@ -10,11 +10,9 @@ #import "AvatarViewHelper.h" #import "AVAudioSession+OWS.h" #import "NotificationSettingsViewController.h" -#import "OWSAnyTouchGestureRecognizer.h" #import "OWSAudioPlayer.h" #import "OWSBezierPathView.h" #import "OWSConversationSettingsViewController.h" -#import "OWSDatabaseMigration.h" #import "OWSMessageTimerView.h" #import "OWSNavigationController.h" #import "OWSProgressView.h" @@ -32,7 +30,6 @@ #import #import #import -#import #import #import #import diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 9d8adff9c..4c2df0832 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Fehler"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index ca173d7f7..75ed86d1a 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -631,3 +631,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 6ba0ab4f5..c6d4c4720 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Fallo"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 710e5f4e7..af92c8e01 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "خطاء"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index c50297985..986517c15 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index bd4752b8b..55d711fce 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Erreur"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index babc35e71..ccd1b2a66 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 5705b0b19..7dc53a8c8 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index f6c91ddf2..02a4dbb4d 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Galat"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index f73ace460..9e11deb4a 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Errore"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 1717a4e37..1dbafa69c 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "エラー"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 22ae40e3e..877f962b9 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 9ee0cb99e..504bd1e68 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Błąd"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index c74587594..07c3a29e1 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Erro"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 16f8f85e2..f9ed4e582 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Ошибка"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 6e6dbc59f..a1cf256f7 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -622,3 +622,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 49fae9bf2..1f7490d42 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 7bc60b944..33bc3d2bf 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 9095870a8..a8f58a441 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 6b4204a04..e11143ca7 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 5f1146f43..cf53c9623 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "Error"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 83d8edef6..b13723ac2 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -621,3 +621,5 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "ALERT_ERROR_TITLE" = "错误"; +"LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database"; diff --git a/Session/Settings/PrivacySettingsTableViewController.m b/Session/Settings/PrivacySettingsTableViewController.m index 153ba68ab..08c3d5197 100644 --- a/Session/Settings/PrivacySettingsTableViewController.m +++ b/Session/Settings/PrivacySettingsTableViewController.m @@ -6,7 +6,6 @@ #import "Session-Swift.h" #import -#import #import #import diff --git a/Session/Settings/ShareLogsModal.swift b/Session/Settings/ShareLogsModal.swift index 0a93acff1..4bd28d6b0 100644 --- a/Session/Settings/ShareLogsModal.swift +++ b/Session/Settings/ShareLogsModal.swift @@ -55,17 +55,24 @@ final class ShareLogsModal : Modal { contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing) } - // MARK: Interaction + // MARK: - Interaction + @objc private func shareLogs() { + ShareLogsModal.shareLogs(from: self) + } + + public static func shareLogs(from viewController: UIViewController, onShareComplete: (() -> ())? = nil) { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion) \(version)") DDLog.flushLog() let logFilePaths = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths if let latestLogFilePath = logFilePaths.first { let latestLogFileURL = URL(fileURLWithPath: latestLogFilePath) - self.dismiss(animated: true, completion: { + + viewController.dismiss(animated: true, completion: { if let vc = CurrentAppContext().frontmostViewController() { let shareVC = UIActivityViewController(activityItems: [ latestLogFileURL ], applicationActivities: nil) + shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() } if UIDevice.current.isIPad { shareVC.excludedActivityTypes = [] shareVC.popoverPresentationController?.permittedArrowDirections = [] diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift index e25f9541e..5b4dcf4ad 100644 --- a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -5,9 +5,10 @@ import GRDB import SessionUtilitiesKit enum _001_InitialSetupMigration: Migration { + static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "initialSetup" - static let minExpectedRunDuration: TimeInterval = 0.1 static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 static func migrate(_ db: Database) throws { // Define the tokenizer to be used in all the FTS tables diff --git a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift index 55b0bec75..dac6eaebc 100644 --- a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -9,9 +9,10 @@ import SessionSnodeKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` enum _002_SetupStandardJobs: Migration { + static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "SetupStandardJobs" - static let minExpectedRunDuration: TimeInterval = 0.1 static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 static func migrate(_ db: Database) throws { // Start by adding the jobs that don't have collections (in the jobs like these diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 7cb7042e5..38d28b322 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -10,15 +10,19 @@ import SessionSnodeKit // Note: Looks like the oldest iOS device we support (min iOS 13.0) has 2Gb of RAM, processing // ~250k messages and ~1000 threads seems to take up enum _003_YDBToGRDBMigration: Migration { + static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "YDBToGRDBMigration" - static let minExpectedRunDuration: TimeInterval = 20 static let needsConfigSync: Bool = true + static let minExpectedRunDuration: TimeInterval = 20 static func migrate(_ db: Database) throws { - let targetIdentifier: TargetMigrations.Identifier = .messagingKit + guard let dbConnection: YapDatabaseConnection = SUKLegacy.newDatabaseConnection() else { + SNLog("[Migration Warning] No legacy database, skipping \(target.key(with: self))") + return + } + + // MARK: - Read from Legacy Database - // MARK: - Process Contacts, Threads & Interactions - print("RAWR [\(Date().timeIntervalSince1970)] - SessionMessagingKit migration - Start") var shouldFailMigration: Bool = false var legacyMigrations: Set = [] var contacts: Set = [] @@ -48,89 +52,24 @@ enum _003_YDBToGRDBMigration: Migration { var outgoingReadReceiptsTimestampsMs: [String: Set] = [:] var receivedMessageTimestamps: Set = [] - // Map the Legacy types for the NSKeyedUnarchiver - NSKeyedUnarchiver.setClass( - SMKLegacy._Thread.self, - forClassName: "TSThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ContactThread.self, - forClassName: "TSContactThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._GroupThread.self, - forClassName: "TSGroupThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._GroupModel.self, - forClassName: "TSGroupModel" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._Contact.self, - forClassName: "SNContact" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBInteraction.self, - forClassName: "TSInteraction" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBMessage.self, - forClassName: "TSMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBQuotedMessage.self, - forClassName: "TSQuotedMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBQuotedMessage._DBAttachmentInfo.self, - forClassName: "OWSAttachmentInfo" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBLinkPreview.self, - forClassName: "SessionServiceKit.OWSLinkPreview" // Very old legacy name - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBLinkPreview.self, - forClassName: "SessionMessagingKit.OWSLinkPreview" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBIncomingMessage.self, - forClassName: "TSIncomingMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBOutgoingMessage.self, - forClassName: "TSOutgoingMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBOutgoingMessageRecipientState.self, - forClassName: "TSOutgoingMessageRecipientState" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DBInfoMessage.self, - forClassName: "TSInfoMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DisappearingConfigurationUpdateInfoMessage.self, - forClassName: "OWSDisappearingConfigurationUpdateInfoMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._Attachment.self, - forClassName: "TSAttachment" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._AttachmentStream.self, - forClassName: "TSAttachmentStream" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._AttachmentPointer.self, - forClassName: "TSAttachmentPointer" - ) + var notifyPushServerJobs: Set = [] + var messageReceiveJobs: Set = [] + var messageSendJobs: Set = [] + var attachmentUploadJobs: Set = [] + var attachmentDownloadJobs: Set = [] + + var legacyPreferences: [String: Any] = [:] + + // Map the Legacy types for the NSKeyedUnarchivez + self.mapLegacyTypesForNSKeyedUnarchiver() - Storage.read { transaction in + dbConnection.read { transaction in + // MARK: --Migrations + // Process the migrations (we don't want to bother running the old migrations as it would be // a waste of time, rather we include the logic from the old migrations in here and make the // same changes if the migration hasn't already run) - transaction.enumerateRows(inCollection: SMKLegacy.databaseMigrationCollection) { key, _, _, _ in + transaction.enumerateKeys(inCollection: SMKLegacy.databaseMigrationCollection) { key, _ in guard let legacyMigration: SMKLegacy._DBMigration = SMKLegacy._DBMigration(rawValue: key) else { SNLog("[Migration Error] Found unknown migration") shouldFailMigration = true @@ -139,23 +78,28 @@ enum _003_YDBToGRDBMigration: Migration { legacyMigrations.insert(legacyMigration) } + GRDBStorage.shared.update(progress: 0.01, for: self, in: target) + + // MARK: --Contacts + + SNLog("[Migration Info] \(target.key(with: self)) - Processing Contacts") - // Process the Contacts transaction.enumerateRows(inCollection: SMKLegacy.contactCollection) { _, object, _, _ in guard let contact = object as? SMKLegacy._Contact else { return } contacts.insert(contact) validProfileIds.insert(contact.sessionID) } - // Process legacy blocked contacts legacyBlockedSessionIds = Set(transaction.object( forKey: SMKLegacy.blockedPhoneNumbersKey, inCollection: SMKLegacy.blockListCollection ) as? [String] ?? []) - - print("RAWR [\(Date().timeIntervalSince1970)] - Process threads - Start") + GRDBStorage.shared.update(progress: 0.02, for: self, in: target) + + // MARK: --Threads + + SNLog("[Migration Info] \(target.key(with: self)) - Processing Threads") - // Process the threads transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.threadCollection) { key, object, _ in guard let thread: SMKLegacy._Thread = object as? SMKLegacy._Thread else { return } @@ -237,10 +181,12 @@ enum _003_YDBToGRDBMigration: Migration { openGroupLastDeletionServerId[thread.uniqueId] = transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupLastDeletionServerIDCollection) as? Int64 } } - print("RAWR [\(Date().timeIntervalSince1970)] - Process threads - End") + GRDBStorage.shared.update(progress: 0.04, for: self, in: target) + + // MARK: --Interactions + + SNLog("[Migration Info] \(target.key(with: self)) - Processing Interactions") - // Process interactions - print("RAWR [\(Date().timeIntervalSince1970)] - Process interactions - Start") transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.interactionCollection) { _, object, _ in guard let interaction: SMKLegacy._DBInteraction = object as? SMKLegacy._DBInteraction else { SNLog("[Migration Error] Unable to process interaction") @@ -251,10 +197,12 @@ enum _003_YDBToGRDBMigration: Migration { interactions[interaction.uniqueThreadId] = (interactions[interaction.uniqueThreadId] ?? []) .appending(interaction) } - print("RAWR [\(Date().timeIntervalSince1970)] - Process interactions - End") + GRDBStorage.shared.update(progress: 0.19, for: self, in: target) + + // MARK: --Attachments + + SNLog("[Migration Info] \(target.key(with: self)) - Processing Attachments") - // Process attachments - print("RAWR [\(Date().timeIntervalSince1970)] - Process attachments - Start") transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.attachmentsCollection) { key, object, _ in guard let attachment: SMKLegacy._Attachment = object as? SMKLegacy._Attachment else { SNLog("[Migration Error] Unable to process attachment") @@ -264,9 +212,10 @@ enum _003_YDBToGRDBMigration: Migration { attachments[key] = attachment } - print("RAWR [\(Date().timeIntervalSince1970)] - Process attachments - End") + GRDBStorage.shared.update(progress: 0.21, for: self, in: target) + + // MARK: --Read Receipts - // Process read receipts transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.outgoingReadReceiptManagerCollection) { key, object, _ in guard let timestampsMs: Set = object as? Set else { return } @@ -284,6 +233,84 @@ enum _003_YDBToGRDBMigration: Migration { .defaulting(to: []) .asSet() ) + + // MARK: --Jobs + + SNLog("[Migration Info] \(target.key(with: self)) - Processing Jobs") + + transaction.enumerateRows(inCollection: SMKLegacy.notifyPushServerJobCollection) { _, object, _, _ in + guard let job = object as? SMKLegacy._NotifyPNServerJob else { return } + notifyPushServerJobs.insert(job) + } + + transaction.enumerateRows(inCollection: SMKLegacy.messageReceiveJobCollection) { _, object, _, _ in + guard let job = object as? SMKLegacy._MessageReceiveJob else { return } + messageReceiveJobs.insert(job) + } + + transaction.enumerateRows(inCollection: SMKLegacy.messageSendJobCollection) { _, object, _, _ in + guard let job = object as? SMKLegacy._MessageSendJob else { return } + messageSendJobs.insert(job) + } + + transaction.enumerateRows(inCollection: SMKLegacy.attachmentUploadJobCollection) { _, object, _, _ in + guard let job = object as? SMKLegacy._AttachmentUploadJob else { return } + attachmentUploadJobs.insert(job) + } + + transaction.enumerateRows(inCollection: SMKLegacy.attachmentDownloadJobCollection) { _, object, _, _ in + guard let job = object as? SMKLegacy._AttachmentDownloadJob else { return } + attachmentDownloadJobs.insert(job) + } + GRDBStorage.shared.update(progress: 0.22, for: self, in: target) + + // MARK: --Preferences + + SNLog("[Migration Info] \(target.key(with: self)) - Processing Preferences") + + transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.preferencesCollection) { key, object, _ in + legacyPreferences[key] = object + } + + transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.additionalPreferencesCollection) { key, object, _ in + legacyPreferences[key] = object + } + + // Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value + // for the notification sound so catch it and default + let globalNotificationSoundValue: Int32 = transaction.int( + forKey: SMKLegacy.soundsGlobalNotificationKey, + inCollection: SMKLegacy.soundsStorageNotificationCollection + ) + legacyPreferences[SMKLegacy.soundsGlobalNotificationKey] = (globalNotificationSoundValue > 0 ? + Int(globalNotificationSoundValue) : + Preferences.Sound.defaultNotificationSound.rawValue + ) + + legacyPreferences[SMKLegacy.readReceiptManagerAreReadReceiptsEnabled] = transaction.bool( + forKey: SMKLegacy.readReceiptManagerAreReadReceiptsEnabled, + inCollection: SMKLegacy.readReceiptManagerCollection, + defaultValue: false + ) + + legacyPreferences[SMKLegacy.typingIndicatorsEnabledKey] = transaction.bool( + forKey: SMKLegacy.typingIndicatorsEnabledKey, + inCollection: SMKLegacy.typingIndicatorsCollection, + defaultValue: false + ) + + legacyPreferences[SMKLegacy.screenLockIsScreenLockEnabledKey] = transaction.bool( + forKey: SMKLegacy.screenLockIsScreenLockEnabledKey, + inCollection: SMKLegacy.screenLockCollection, + defaultValue: false + ) + + legacyPreferences[SMKLegacy.screenLockScreenLockTimeoutSecondsKey] = transaction.double( + forKey: SMKLegacy.screenLockScreenLockTimeoutSecondsKey, + inCollection: SMKLegacy.screenLockCollection, + defaultValue: (15 * 60) + ) + GRDBStorage.shared.update(progress: 0.23, for: self, in: target) } // We can't properly throw within the 'enumerateKeysAndObjects' block so have to throw here @@ -295,10 +322,14 @@ enum _003_YDBToGRDBMigration: Migration { // MARK: - Insert Contacts + SNLog("[Migration Info] \(target.key(with: self)) - Inserting Contacts") + try autoreleasepool { - let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) + // Values for contact progress + let contactStartProgress: CGFloat = 0.23 + let progressPerContact: CGFloat = (0.05 / CGFloat(contacts.count)) - try contacts.forEach { legacyContact in + try contacts.enumerated().forEach { index, legacyContact in let isCurrentUser: Bool = (legacyContact.sessionID == currentUserPublicKey) let contactThreadId: String = SMKLegacy._ContactThread.threadId(from: legacyContact.sessionID) @@ -368,12 +399,25 @@ enum _003_YDBToGRDBMigration: Migration { hasBeenBlocked: (!isCurrentUser && (legacyContact.hasBeenBlocked || legacyContact.isBlocked)) ).insert(db) } + + // Increment the progress for each contact + GRDBStorage.shared.update( + progress: contactStartProgress + (progressPerContact * CGFloat(index + 1)), + for: self, + in: target + ) } } + // Clear out processed data (give the memory a change to be freed) + contacts = [] + legacyBlockedSessionIds = [] + contactThreadIds = [] + // MARK: - Insert Threads - print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - Start") + SNLog("[Migration Info] \(target.key(with: self)) - Inserting Threads & Interactions") + var legacyInteractionToIdMap: [String: Int64] = [:] var legacyInteractionIdentifierToIdMap: [String: Int64] = [:] var legacyInteractionIdentifierToIdFallbackMap: [String: Int64] = [:] @@ -411,6 +455,12 @@ enum _003_YDBToGRDBMigration: Migration { .joined(separator: "-") } + // Values for thread progress + var interactionCounter: CGFloat = 0 + let allInteractionsCount: Int = interactions.map { $0.value.count }.reduce(0, +) + let threadInteractionsStartProgress: CGFloat = 0.28 + let progressPerInteraction: CGFloat = (0.70 / CGFloat(allInteractionsCount)) + // Sort by id just so we can make the migration process more determinstic try legacyThreads.sorted(by: { lhs, rhs in lhs.uniqueId < rhs.uniqueId }).forEach { legacyThread in guard let threadId: String = legacyThreadIdToIdMap[legacyThread.uniqueId] else { @@ -921,25 +971,22 @@ enum _003_YDBToGRDBMigration: Migration { attachmentId: attachmentId ).insert(db) } - } + + // Increment the progress for each contact + GRDBStorage.shared.update( + progress: ( + threadInteractionsStartProgress + + (progressPerInteraction * (interactionCounter + 1)) + ), + for: self, + in: target + ) + interactionCounter += 1 + } } } - // Insert a 'ControlMessageProcessRecord' for any remaining 'receivedMessageTimestamp' - // entries as "legacy" - try ControlMessageProcessRecord.generateLegacyProcessRecords( - db, - receivedMessageTimestamps: receivedMessageTimestamps.map { Int64($0) } - ) - - print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - End") - // Clear out processed data (give the memory a change to be freed) - - contacts = [] - legacyBlockedSessionIds = [] - contactThreadIds = [] - legacyThreads = [] disappearingMessagesConfiguration = [:] @@ -957,150 +1004,24 @@ enum _003_YDBToGRDBMigration: Migration { interactions = [:] attachments = [:] + + // MARK: --Received Message Timestamps + + // Insert a 'ControlMessageProcessRecord' for any remaining 'receivedMessageTimestamp' + // entries as "legacy" + try ControlMessageProcessRecord.generateLegacyProcessRecords( + db, + receivedMessageTimestamps: receivedMessageTimestamps.map { Int64($0) } + ) + + // Clear out processed data (give the memory a change to be freed) receivedMessageTimestamps = [] - // MARK: - Process Legacy Jobs + // MARK: - Insert Jobs - print("RAWR [\(Date().timeIntervalSince1970)] - Process jobs - Start") + SNLog("[Migration Info] \(target.key(with: self)) - Inserting Jobs") - var notifyPushServerJobs: Set = [] - var messageReceiveJobs: Set = [] - var messageSendJobs: Set = [] - var attachmentUploadJobs: Set = [] - var attachmentDownloadJobs: Set = [] - - // Map the Legacy types for the NSKeyedUnarchiver - NSKeyedUnarchiver.setClass( - SMKLegacy._NotifyPNServerJob.self, - forClassName: "SessionMessagingKit.NotifyPNServerJob" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._NotifyPNServerJob._SnodeMessage.self, - forClassName: "SessionSnodeKit.SnodeMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._MessageSendJob.self, - forClassName: "SessionMessagingKit.SNMessageSendJob" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._MessageReceiveJob.self, - forClassName: "SessionMessagingKit.MessageReceiveJob" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._AttachmentUploadJob.self, - forClassName: "SessionMessagingKit.AttachmentUploadJob" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._AttachmentDownloadJob.self, - forClassName: "SessionMessagingKit.AttachmentDownloadJob" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._Message.self, - forClassName: "SNMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._VisibleMessage.self, - forClassName: "SNVisibleMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._Quote.self, - forClassName: "SNQuote" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._LinkPreview.self, - forClassName: "SNLinkPreview" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._Profile.self, - forClassName: "SNProfile" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._OpenGroupInvitation.self, - forClassName: "SNOpenGroupInvitation" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ControlMessage.self, - forClassName: "SNControlMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ReadReceipt.self, - forClassName: "SNReadReceipt" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._TypingIndicator.self, - forClassName: "SNTypingIndicator" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ClosedGroupControlMessage.self, - forClassName: "SessionMessagingKit.ClosedGroupControlMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ClosedGroupControlMessage._KeyPairWrapper.self, - forClassName: "ClosedGroupControlMessage.SNKeyPairWrapper" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._DataExtractionNotification.self, - forClassName: "SessionMessagingKit.DataExtractionNotification" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ExpirationTimerUpdate.self, - forClassName: "SNExpirationTimerUpdate" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ConfigurationMessage.self, - forClassName: "SNConfigurationMessage" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._CMClosedGroup.self, - forClassName: "SNClosedGroup" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._CMContact.self, - forClassName: "SNConfigurationMessage.SNConfigurationMessageContact" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._UnsendRequest.self, - forClassName: "SNUnsendRequest" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._MessageRequestResponse.self, - forClassName: "SNMessageRequestResponse" - ) - - Storage.read { transaction in - transaction.enumerateRows(inCollection: SMKLegacy.notifyPushServerJobCollection) { _, object, _, _ in - guard let job = object as? SMKLegacy._NotifyPNServerJob else { return } - notifyPushServerJobs.insert(job) - } - - transaction.enumerateRows(inCollection: SMKLegacy.messageReceiveJobCollection) { _, object, _, _ in - guard let job = object as? SMKLegacy._MessageReceiveJob else { return } - messageReceiveJobs.insert(job) - } - - transaction.enumerateRows(inCollection: SMKLegacy.messageSendJobCollection) { _, object, _, _ in - guard let job = object as? SMKLegacy._MessageSendJob else { return } - messageSendJobs.insert(job) - } - - transaction.enumerateRows(inCollection: SMKLegacy.attachmentUploadJobCollection) { _, object, _, _ in - guard let job = object as? SMKLegacy._AttachmentUploadJob else { return } - attachmentUploadJobs.insert(job) - } - - transaction.enumerateRows(inCollection: SMKLegacy.attachmentDownloadJobCollection) { _, object, _, _ in - guard let job = object as? SMKLegacy._AttachmentDownloadJob else { return } - attachmentDownloadJobs.insert(job) - } - } - - print("RAWR [\(Date().timeIntervalSince1970)] - Process jobs - End") - - // MARK: - Insert Jobs - - print("RAWR [\(Date().timeIntervalSince1970)] - Process job inserts - Start") - - // MARK: - --notifyPushServer + // MARK: --notifyPushServer try autoreleasepool { try notifyPushServerJobs.forEach { legacyJob in @@ -1123,7 +1044,7 @@ enum _003_YDBToGRDBMigration: Migration { } } - // MARK: - --messageReceive + // MARK: --messageReceive try autoreleasepool { try messageReceiveJobs.forEach { legacyJob in @@ -1172,7 +1093,7 @@ enum _003_YDBToGRDBMigration: Migration { } } - // MARK: - --messageSend + // MARK: --messageSend var messageSendJobLegacyMap: [String: Job] = [:] @@ -1266,7 +1187,7 @@ enum _003_YDBToGRDBMigration: Migration { } } - // MARK: - --attachmentUpload + // MARK: --attachmentUpload try autoreleasepool { try attachmentUploadJobs.forEach { legacyJob in @@ -1300,7 +1221,7 @@ enum _003_YDBToGRDBMigration: Migration { } } - // MARK: - --attachmentDownload + // MARK: --attachmentDownload try autoreleasepool { try attachmentDownloadJobs.forEach { legacyJob in @@ -1327,7 +1248,7 @@ enum _003_YDBToGRDBMigration: Migration { } } - // MARK: - --sendReadReceipts + // MARK: --sendReadReceipts try autoreleasepool { try outgoingReadReceiptsTimestampsMs.forEach { threadId, timestampsMs in @@ -1342,59 +1263,11 @@ enum _003_YDBToGRDBMigration: Migration { )?.inserted(db) } } + GRDBStorage.shared.update(progress: 0.99, for: self, in: target) - print("RAWR [\(Date().timeIntervalSince1970)] - Process job inserts - End") - - // MARK: - Process Preferences + // MARK: - Preferences - print("RAWR [\(Date().timeIntervalSince1970)] - Process preferences inserts - Start") - - var legacyPreferences: [String: Any] = [:] - - Storage.read { transaction in - transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.preferencesCollection) { key, object, _ in - legacyPreferences[key] = object - } - - transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.additionalPreferencesCollection) { key, object, _ in - legacyPreferences[key] = object - } - - // Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value - // for the notification sound so catch it and default - let globalNotificationSoundValue: Int32 = transaction.int( - forKey: SMKLegacy.soundsGlobalNotificationKey, - inCollection: SMKLegacy.soundsStorageNotificationCollection - ) - legacyPreferences[SMKLegacy.soundsGlobalNotificationKey] = (globalNotificationSoundValue > 0 ? - Int(globalNotificationSoundValue) : - Preferences.Sound.defaultNotificationSound.rawValue - ) - - legacyPreferences[SMKLegacy.readReceiptManagerAreReadReceiptsEnabled] = transaction.bool( - forKey: SMKLegacy.readReceiptManagerAreReadReceiptsEnabled, - inCollection: SMKLegacy.readReceiptManagerCollection, - defaultValue: false - ) - - legacyPreferences[SMKLegacy.typingIndicatorsEnabledKey] = transaction.bool( - forKey: SMKLegacy.typingIndicatorsEnabledKey, - inCollection: SMKLegacy.typingIndicatorsCollection, - defaultValue: false - ) - - legacyPreferences[SMKLegacy.screenLockIsScreenLockEnabledKey] = transaction.bool( - forKey: SMKLegacy.screenLockIsScreenLockEnabledKey, - inCollection: SMKLegacy.screenLockCollection, - defaultValue: false - ) - - legacyPreferences[SMKLegacy.screenLockScreenLockTimeoutSecondsKey] = transaction.double( - forKey: SMKLegacy.screenLockScreenLockTimeoutSecondsKey, - inCollection: SMKLegacy.screenLockCollection, - defaultValue: (15 * 60) - ) - } + SNLog("[Migration Info] \(target.key(with: self)) - Inserting Preferences") db[.defaultNotificationSound] = Preferences.Sound(rawValue: legacyPreferences[SMKLegacy.soundsGlobalNotificationKey] as? Int ?? -1) .defaulting(to: Preferences.Sound.defaultNotificationSound) @@ -1425,10 +1298,6 @@ enum _003_YDBToGRDBMigration: Migration { db[.hasSavedThread] = (legacyPreferences[SMKLegacy.preferencesKeyHasSavedThreadKey] as? Bool == true) db[.hasSentAMessage] = (legacyPreferences[SMKLegacy.preferencesKeyHasSentAMessageKey] as? Bool == true) db[.isReadyForAppExtensions] = CurrentAppContext().appUserDefaults().bool(forKey: SMKLegacy.preferencesKeyIsReadyForAppExtensions) - - print("RAWR [\(Date().timeIntervalSince1970)] - Process preferences inserts - End") - - print("RAWR Done!!!") } // MARK: - Convenience @@ -1582,4 +1451,179 @@ enum _003_YDBToGRDBMigration: Migration { return legacyAttachmentId } + + private static func mapLegacyTypesForNSKeyedUnarchiver() { + NSKeyedUnarchiver.setClass( + SMKLegacy._Thread.self, + forClassName: "TSThread" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._ContactThread.self, + forClassName: "TSContactThread" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._GroupThread.self, + forClassName: "TSGroupThread" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._GroupModel.self, + forClassName: "TSGroupModel" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._Contact.self, + forClassName: "SNContact" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBInteraction.self, + forClassName: "TSInteraction" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBMessage.self, + forClassName: "TSMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBQuotedMessage.self, + forClassName: "TSQuotedMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBQuotedMessage._DBAttachmentInfo.self, + forClassName: "OWSAttachmentInfo" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBLinkPreview.self, + forClassName: "SessionServiceKit.OWSLinkPreview" // Very old legacy name + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBLinkPreview.self, + forClassName: "SessionMessagingKit.OWSLinkPreview" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBIncomingMessage.self, + forClassName: "TSIncomingMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBOutgoingMessage.self, + forClassName: "TSOutgoingMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBOutgoingMessageRecipientState.self, + forClassName: "TSOutgoingMessageRecipientState" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DBInfoMessage.self, + forClassName: "TSInfoMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DisappearingConfigurationUpdateInfoMessage.self, + forClassName: "OWSDisappearingConfigurationUpdateInfoMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._Attachment.self, + forClassName: "TSAttachment" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._AttachmentStream.self, + forClassName: "TSAttachmentStream" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._AttachmentPointer.self, + forClassName: "TSAttachmentPointer" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._NotifyPNServerJob.self, + forClassName: "SessionMessagingKit.NotifyPNServerJob" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._NotifyPNServerJob._SnodeMessage.self, + forClassName: "SessionSnodeKit.SnodeMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._MessageSendJob.self, + forClassName: "SessionMessagingKit.SNMessageSendJob" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._MessageReceiveJob.self, + forClassName: "SessionMessagingKit.MessageReceiveJob" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._AttachmentUploadJob.self, + forClassName: "SessionMessagingKit.AttachmentUploadJob" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._AttachmentDownloadJob.self, + forClassName: "SessionMessagingKit.AttachmentDownloadJob" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._Message.self, + forClassName: "SNMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._VisibleMessage.self, + forClassName: "SNVisibleMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._Quote.self, + forClassName: "SNQuote" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._LinkPreview.self, + forClassName: "SNLinkPreview" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._Profile.self, + forClassName: "SNProfile" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._OpenGroupInvitation.self, + forClassName: "SNOpenGroupInvitation" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._ControlMessage.self, + forClassName: "SNControlMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._ReadReceipt.self, + forClassName: "SNReadReceipt" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._TypingIndicator.self, + forClassName: "SNTypingIndicator" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._ClosedGroupControlMessage.self, + forClassName: "SessionMessagingKit.ClosedGroupControlMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._ClosedGroupControlMessage._KeyPairWrapper.self, + forClassName: "ClosedGroupControlMessage.SNKeyPairWrapper" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._DataExtractionNotification.self, + forClassName: "SessionMessagingKit.DataExtractionNotification" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._ExpirationTimerUpdate.self, + forClassName: "SNExpirationTimerUpdate" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._ConfigurationMessage.self, + forClassName: "SNConfigurationMessage" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._CMClosedGroup.self, + forClassName: "SNClosedGroup" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._CMContact.self, + forClassName: "SNConfigurationMessage.SNConfigurationMessageContact" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._UnsendRequest.self, + forClassName: "SNUnsendRequest" + ) + NSKeyedUnarchiver.setClass( + SMKLegacy._MessageRequestResponse.self, + forClassName: "SNMessageRequestResponse" + ) + } } diff --git a/SessionMessagingKit/Database/Models/RecipientState.swift b/SessionMessagingKit/Database/Models/RecipientState.swift index e56c3f05d..01bb0a1b0 100644 --- a/SessionMessagingKit/Database/Models/RecipientState.swift +++ b/SessionMessagingKit/Database/Models/RecipientState.swift @@ -122,7 +122,6 @@ public extension RecipientState { public extension RecipientState { static func selectInteractionState(tableLiteral: SQL, idColumnLiteral: SQL) -> SQL { - let interaction: TypedTableAlias = TypedTableAlias() let recipientState: TypedTableAlias = TypedTableAlias() return """ @@ -132,7 +131,6 @@ public extension RecipientState { \(recipientState[.state]), \(recipientState[.mostRecentFailureText]) FROM \(RecipientState.self) - JOIN \(Interaction.self) ON \(interaction[.id]) = \(recipientState[.interactionId]) WHERE \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) -- Ignore 'skipped' ORDER BY -- If there is a single 'sending' then should be 'sending', otherwise if there is a single diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 48c643611..c2f517965 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -209,22 +209,46 @@ public extension SessionThread { // MARK: - Convenience public extension SessionThread { + static func unreadMessageRequestsCountQuery(userPublicKey: String) -> SQLRequest { + let thread: TypedTableAlias = TypedTableAlias() + let contact: TypedTableAlias = TypedTableAlias() + let interaction: TypedTableAlias = TypedTableAlias() + + let unreadInteractionLiteral: SQL = SQL(stringLiteral: "unreadInteraction") + let interactionThreadIdColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name) + + return """ + SELECT COUNT(\(thread[.id])) + FROM \(SessionThread.self) + JOIN ( + SELECT \(interaction[.threadId]) + FROM \(Interaction.self) + WHERE \(interaction[.wasRead]) = false + GROUP BY \(interaction[.threadId]) + ) AS \(unreadInteractionLiteral) ON \(unreadInteractionLiteral).\(interactionThreadIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) + WHERE ( + \(SessionThread.isMessageRequest(userPublicKey: userPublicKey)) + ) + """ + } + /// This method can be used to filter a thread query to only include messages requests /// /// **Note:** In order to use this filter you **MUST** have a `joining(required/optional:)` to the /// `SessionThread.contact` association or it won't work static func isMessageRequest(userPublicKey: String) -> SQLSpecificExpressible { - let threadAlias: TypedTableAlias = TypedTableAlias() - let contactAlias: TypedTableAlias = TypedTableAlias() + let thread: TypedTableAlias = TypedTableAlias() + let contact: TypedTableAlias = TypedTableAlias() return SQL( """ - \(threadAlias[.shouldBeVisible]) = true AND - \(SQL("\(threadAlias[.variant]) = \(SessionThread.Variant.contact)")) AND - \(SQL("\(threadAlias[.id]) != \(userPublicKey)")) AND ( + \(thread[.shouldBeVisible]) = true AND + \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND + \(SQL("\(thread[.id]) != \(userPublicKey)")) AND ( /* Note: A '!= true' check doesn't work properly so we need to be explicit */ - \(contactAlias[.isApproved]) IS NULL OR - \(contactAlias[.isApproved]) = false + \(contact[.isApproved]) IS NULL OR + \(contact[.isApproved]) = false ) """ ) diff --git a/SessionMessagingKit/Database/Models/ThreadTypingIndicator.swift b/SessionMessagingKit/Database/Models/ThreadTypingIndicator.swift index 43fed0f54..bad5e96dd 100644 --- a/SessionMessagingKit/Database/Models/ThreadTypingIndicator.swift +++ b/SessionMessagingKit/Database/Models/ThreadTypingIndicator.swift @@ -14,7 +14,7 @@ public struct ThreadTypingIndicator: Codable, FetchableRecord, PersistableRecord private static let thread = belongsTo(SessionThread.self, using: threadForeignKey) public typealias Columns = CodingKeys - public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + public enum CodingKeys: String, CodingKey, ColumnExpression { case threadId case timestampMs } diff --git a/SessionMessagingKit/Meta/SessionMessagingKit.h b/SessionMessagingKit/Meta/SessionMessagingKit.h index 262399fb4..6006d21c1 100644 --- a/SessionMessagingKit/Meta/SessionMessagingKit.h +++ b/SessionMessagingKit/Meta/SessionMessagingKit.h @@ -4,7 +4,6 @@ FOUNDATION_EXPORT double SessionMessagingKitVersionNumber; FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[]; #import -#import #import #import #import diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index 1d0e64eb0..e94e8d9ab 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -6,9 +6,7 @@ import PromiseKit import Sodium import SessionSnodeKit -@objc(LKPoller) -public final class Poller : NSObject { - private let storage = OWSPrimaryStorage.shared() +public final class Poller { private var isPolling: Atomic = Atomic(false) private var usedSnodes = Set() private var pollCount = 0 @@ -27,7 +25,7 @@ public final class Poller : NSObject { // MARK: - Error - private enum Error : LocalizedError { + private enum Error: LocalizedError { case pollLimitReached var localizedDescription: String { @@ -39,7 +37,9 @@ public final class Poller : NSObject { // MARK: - Public API - @objc public func startIfNeeded() { + public init() {} + + public func startIfNeeded() { guard !isPolling.wrappedValue else { return } SNLog("Started polling.") @@ -47,7 +47,7 @@ public final class Poller : NSObject { setUpPolling() } - @objc public func stop() { + public func stop() { SNLog("Stopped polling.") isPolling.mutate { $0 = false } usedSnodes.removeAll() diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index cf780cdcb..e7e5ea9e4 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -7,6 +7,7 @@ import SessionUtilitiesKit fileprivate typealias ViewModel = MessageViewModel fileprivate typealias AttachmentInteractionInfo = MessageViewModel.AttachmentInteractionInfo +fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) @@ -384,9 +385,28 @@ public extension MessageViewModel { } } +// MARK: - TypingIndicatorInfo + +public extension MessageViewModel { + struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable { + public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) + public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) + + public let rowId: Int64 + public let threadId: String + + // MARK: - Identifiable + + public var id: String { threadId } + } +} + // MARK: - Convenience Initialization public extension MessageViewModel { + public static let genericId: Int64 = -2 + public static let typingIndicatorId: Int64 = -2 + // Note: This init method is only used system-created cells or empty states init(isTypingIndicator: Bool = false) { self.threadVariant = .contact @@ -395,8 +415,9 @@ public extension MessageViewModel { // Interaction Info - self.rowId = -1 - self.id = -1 + let targetId: Int64 = (isTypingIndicator ? MessageViewModel.typingIndicatorId : MessageViewModel.genericId) + self.rowId = targetId + self.id = targetId self.variant = .standardOutgoing self.timestampMs = Int64.max self.authorId = "" @@ -473,6 +494,8 @@ extension MessageViewModel { // MARK: - ConversationVC +// MARK: --MessageViewModel + public extension MessageViewModel { static func filterSQL(threadId: String) -> SQL { let interaction: TypedTableAlias = TypedTableAlias() @@ -612,6 +635,8 @@ public extension MessageViewModel { } } +// MARK: --AttachmentInteractionInfo + public extension MessageViewModel.AttachmentInteractionInfo { static let baseQuery: ((SQL?) -> AdaptedFetchRequest>) = { return { additionalFilters -> AdaptedFetchRequest> in @@ -697,3 +722,51 @@ public extension MessageViewModel.AttachmentInteractionInfo { } } } + +// MARK: --TypingIndicatorInfo + +public extension MessageViewModel.TypingIndicatorInfo { + static let baseQuery: ((SQL?) -> SQLRequest) = { + return { additionalFilters -> SQLRequest in + let threadTypingIndicator: TypedTableAlias = TypedTableAlias() + let finalFilterSQL: SQL = { + guard let additionalFilters: SQL = additionalFilters else { + return SQL(stringLiteral: "") + } + + return """ + WHERE \(additionalFilters) + """ + }() + let request: SQLRequest = """ + SELECT + \(threadTypingIndicator.alias[Column.rowID]) AS \(MessageViewModel.TypingIndicatorInfo.rowIdKey), + \(threadTypingIndicator[.threadId]) AS \(MessageViewModel.TypingIndicatorInfo.threadIdKey) + FROM \(ThreadTypingIndicator.self) + \(finalFilterSQL) + """ + + return request + } + }() + + static var joinToViewModelQuerySQL: SQL = { + let interaction: TypedTableAlias = TypedTableAlias() + let threadTypingIndicator: TypedTableAlias = TypedTableAlias() + + return """ + JOIN \(Interaction.self) ON \(interaction[.threadId]) = \(threadTypingIndicator[.threadId]) + """ + }() + + static func createAssociateDataClosure() -> (DataCache, DataCache) -> DataCache { + return { dataCache, pagedDataCache -> DataCache in + guard !dataCache.data.isEmpty else { + return pagedDataCache.deleting(rowIds: [MessageViewModel.typingIndicatorId]) + } + + return pagedDataCache + .upserting(MessageViewModel(isTypingIndicator: true)) + } + } +} diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index a49d37007..29c9e9361 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -264,7 +264,6 @@ public extension SessionThreadViewModel { let interaction: TypedTableAlias = TypedTableAlias() let linkPreview: TypedTableAlias = TypedTableAlias() let interactionAttachment: TypedTableAlias = TypedTableAlias() - let attachment: TypedTableAlias = TypedTableAlias() let unreadCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadUnreadCountString)_table") let unreadMentionCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadUnreadMentionCountString)_table") @@ -274,6 +273,9 @@ public extension SessionThreadViewModel { let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) let authorProfileLiteral: SQL = SQL(stringLiteral: "authorProfile") let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name) + let attachmentVariantColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.variant.name) + let attachmentContentTypeColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.contentType.name) + let attachmentSourceFilenameColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.sourceFilename.name) let firstInteractionAttachmentLiteral: SQL = SQL(stringLiteral: "firstInteractionAttachment") let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) @@ -288,8 +290,9 @@ public extension SessionThreadViewModel { /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 11 - let numColumnsBetweenProfilesAndAttachmentInfo: Int = 10 + let numColumnsBetweenProfilesAndAttachmentInfo: Int = 10 // The attachment info columns will be combined // TODO: Some testing around the subqueries in the joins to see if they impact performance ('Simulator1' device takes ~125ms to complete this query) + let request: SQLRequest = """ SELECT \(thread[.id]) AS \(ViewModel.threadIdKey), @@ -322,7 +325,10 @@ public extension SessionThreadViewModel { -- Default to 'sending' assuming non-processed interaction when null IFNULL(\(interactionStateTableLiteral).\(interactionStateStateColumnLiteral), \(SQL("\(RecipientState.State.sending)"))) AS \(interactionStateTableLiteral), (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey), - \(ViewModel.interactionAttachmentDescriptionInfoKey).*, + \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentIdColumnLiteral), + \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentVariantColumnLiteral), + \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentContentTypeColumnLiteral), + \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentSourceFilenameColumnLiteral), COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey), \(interaction[.authorId]), @@ -406,14 +412,7 @@ public extension SessionThreadViewModel { \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND \(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) ) - LEFT JOIN ( - SELECT - \(attachment[.id]), - \(attachment[.variant]), - \(attachment[.contentType]), - \(attachment[.sourceFilename]) - FROM \(Attachment.self) - ) AS \(ViewModel.interactionAttachmentDescriptionInfoKey) ON \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentIdColumnLiteral) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) + LEFT JOIN \(Attachment.self) AS \(ViewModel.interactionAttachmentDescriptionInfoKey) ON \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentIdColumnLiteral) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) LEFT JOIN ( \(RecipientState.selectInteractionState( tableLiteral: interactionStateTableLiteral, @@ -506,7 +505,6 @@ public extension SessionThreadViewModel { static func conversationQuery(threadId: String, userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() - let typingIndicator: TypedTableAlias = TypedTableAlias() // TODO: Remove this (not needed here - tracked via the messages) let closedGroup: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() @@ -526,7 +524,7 @@ public extension SessionThreadViewModel { /// parse and might throw /// /// Explicitly set default values for the fields ignored for search results - let numColumnsBeforeProfiles: Int = 16 + let numColumnsBeforeProfiles: Int = 15 let request: SQLRequest = """ SELECT \(thread[.id]) AS \(ViewModel.threadIdKey), @@ -557,7 +555,6 @@ public extension SessionThreadViewModel { \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), \(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey), - (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey), \(unreadCountTableLiteral).\(ViewModel.threadUnreadCountKey) AS \(ViewModel.threadUnreadCountKey), \(unreadMentionCountTableLiteral).\(ViewModel.threadUnreadMentionCountKey) AS \(ViewModel.threadUnreadMentionCountKey), \(firstUnreadInteractionTableLiteral).\(interactionIdLiteral) AS \(ViewModel.threadFirstUnreadInteractionIdKey), @@ -578,7 +575,6 @@ public extension SessionThreadViewModel { FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) - LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id]) LEFT JOIN ( SELECT \(interaction[.id]), @@ -870,7 +866,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey) ) - ORDER BY \(Column.rank) + ORDER BY \(Column.rank), \(interaction[.timestampMs].desc) """ return request.adapted { db in diff --git a/SessionMessagingKit/Utilities/OWSWindowManager.m b/SessionMessagingKit/Utilities/OWSWindowManager.m index c1fa33d4f..a1ddc0f2a 100644 --- a/SessionMessagingKit/Utilities/OWSWindowManager.m +++ b/SessionMessagingKit/Utilities/OWSWindowManager.m @@ -3,7 +3,6 @@ // #import "OWSWindowManager.h" -#import "Environment.h" #import #import diff --git a/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h b/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h index 282ed4347..5cf67e263 100644 --- a/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h +++ b/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h @@ -9,12 +9,10 @@ #import #import #import -#import #import #import #import #import -#import #import #import #import diff --git a/SessionShareExtension/ThreadPickerViewModel.swift b/SessionShareExtension/ThreadPickerViewModel.swift index b3090120e..93035647f 100644 --- a/SessionShareExtension/ThreadPickerViewModel.swift +++ b/SessionShareExtension/ThreadPickerViewModel.swift @@ -15,6 +15,11 @@ public class ThreadPickerViewModel { /// /// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static /// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries + /// + /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) + /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own + /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) + /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this public lazy var observableViewData = ValueObservation .trackingConstantRegion { db -> [SessionThreadViewModel] in let userPublicKey: String = getUserHexEncodedPublicKey(db) diff --git a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift index d8b29ae3c..f807c07b1 100644 --- a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -5,9 +5,10 @@ import GRDB import SessionUtilitiesKit enum _001_InitialSetupMigration: Migration { + static let target: TargetMigrations.Identifier = .snodeKit static let identifier: String = "initialSetup" - static let minExpectedRunDuration: TimeInterval = 0.1 static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 static func migrate(_ db: Database) throws { try db.create(table: Snode.self) { t in diff --git a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift index 145cc544b..ad4037ecf 100644 --- a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -7,9 +7,10 @@ import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` enum _002_SetupStandardJobs: Migration { + static let target: TargetMigrations.Identifier = .snodeKit static let identifier: String = "SetupStandardJobs" - static let minExpectedRunDuration: TimeInterval = 0.1 static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 static func migrate(_ db: Database) throws { try autoreleasepool { diff --git a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 0896fa6a5..6f2242741 100644 --- a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -5,17 +5,29 @@ import GRDB import SessionUtilitiesKit enum _003_YDBToGRDBMigration: Migration { + static let target: TargetMigrations.Identifier = .snodeKit static let identifier: String = "YDBToGRDBMigration" - static let minExpectedRunDuration: TimeInterval = 0.2 static let needsConfigSync: Bool = false + /// This migration can take a while if it's a very large database or there are lots of closed groups (want this to account + /// for about 10% of the progress bar so we intentionally have a higher `minExpectedRunDuration` so show more + /// progress during the migration) + static let minExpectedRunDuration: TimeInterval = 2.0 + static func migrate(_ db: Database) throws { - // MARK: - OnionRequestPath, Snode Pool & Swarm + guard let dbConnection: YapDatabaseConnection = SUKLegacy.newDatabaseConnection() else { + SNLog("[Migration Warning] No legacy database, skipping \(target.key(with: self))") + return + } + + // MARK: - Read from Legacy Database // Note: Want to exclude the Snode's we already added from the 'onionRequestPathResult' var snodeResult: Set = [] var snodeSetResult: [String: Set] = [:] var lastSnodePoolRefreshDate: Date? = nil + var lastMessageResults: [String: (hash: String, json: JSON)] = [:] + var receivedMessageResults: [String: Set] = [:] // Map the Legacy types for the NSKeyedUnarchiver NSKeyedUnarchiver.setClass( @@ -23,14 +35,16 @@ enum _003_YDBToGRDBMigration: Migration { forClassName: "SessionSnodeKit.Snode" ) - Storage.read { transaction in - // Process the lastSnodePoolRefreshDate + dbConnection.read { transaction in + // MARK: --lastSnodePoolRefreshDate + lastSnodePoolRefreshDate = transaction.object( forKey: SSKLegacy.lastSnodePoolRefreshDateKey, inCollection: SSKLegacy.lastSnodePoolRefreshDateCollection ) as? Date - // Process the OnionRequestPaths + // MARK: --OnionRequestPaths + if let path0Snode0 = transaction.object(forKey: "0-0", inCollection: SSKLegacy.onionRequestPathCollection) as? SSKLegacy.Snode, let path0Snode1 = transaction.object(forKey: "0-1", inCollection: SSKLegacy.onionRequestPathCollection) as? SSKLegacy.Snode, @@ -52,21 +66,44 @@ enum _003_YDBToGRDBMigration: Migration { snodeSetResult["\(SnodeSet.onionRequestPathPrefix)1"] = [ path1Snode0, path1Snode1, path1Snode2 ] } } + GRDBStorage.shared.update(progress: 0.02, for: self, in: target) + + // MARK: --SnodePool - // Process the SnodePool transaction.enumerateKeysAndObjects(inCollection: SSKLegacy.snodePoolCollection) { _, object, _ in guard let snode = object as? SSKLegacy.Snode else { return } snodeResult.insert(snode) } - // Process the Swarms - var swarmCollections: Set = [] + // MARK: --Swarms + // Note: There is no index on the collection column so unfortunately it takes the same amount of + // time to enumerate through all collections as it does to just get the count of collections, as + // a result if the database is very large this part can be slow (~15s with 2,000,000 rows) - we + // want to show some kind of progress while doing this enumeration so the below code includes a + // number of rough values to show some kind of progression while the enumeration occurs (most users + // won't run into issues with this at all) + var swarmCollections: Set = [] + let startProgress: CGFloat = 0.02 + let swarmCompleteProgress: CGFloat = 0.90 + let interEnumerationMaxProgress: CGFloat = ((swarmCompleteProgress - startProgress) * 0.8) + let maxCollectionsEstimate: CGFloat = 1000 + let numCollectionsToTriggerProgressUpdate: CGFloat = 20 + var collectionIndex: CGFloat = 0 + var oldProgress: CGFloat = startProgress transaction.enumerateCollections { collectionName, _ in if collectionName.starts(with: SSKLegacy.swarmCollectionPrefix) { swarmCollections.insert(collectionName.substring(from: SSKLegacy.swarmCollectionPrefix.count)) } + + collectionIndex += 1 + + if collectionIndex.truncatingRemainder(dividingBy: numCollectionsToTriggerProgressUpdate) == 0 { + oldProgress = (startProgress + (interEnumerationMaxProgress * (collectionIndex / maxCollectionsEstimate))) + GRDBStorage.shared.update(progress: oldProgress, for: self, in: target) + } } + GRDBStorage.shared.update(progress: swarmCompleteProgress, for: self, in: target) for swarmCollection in swarmCollections { let collection: String = "\(SSKLegacy.swarmCollectionPrefix)\(swarmCollection)" @@ -77,13 +114,39 @@ enum _003_YDBToGRDBMigration: Migration { snodeSetResult[swarmCollection] = (snodeSetResult[swarmCollection] ?? Set()).inserting(snode) } } + GRDBStorage.shared.update(progress: 0.92, for: self, in: target) + + // MARK: --Received message hashes + + transaction.enumerateKeysAndObjects(inCollection: SSKLegacy.receivedMessagesCollection) { key, object, _ in + guard let hashSet = object as? Set else { return } + receivedMessageResults[key] = hashSet + } + GRDBStorage.shared.update(progress: 0.93, for: self, in: target) + + // MARK: --Last message info + + transaction.enumerateKeysAndObjects(inCollection: SSKLegacy.lastMessageHashCollection) { key, object, _ in + guard let lastMessageJson = object as? JSON else { return } + guard let lastMessageHash: String = lastMessageJson["hash"] as? String else { return } + + // Note: We remove the value from 'receivedMessageResults' as we want to try and use + // it's actual 'expirationDate' value + lastMessageResults[key] = (lastMessageHash, lastMessageJson) + receivedMessageResults[key] = receivedMessageResults[key]?.removing(lastMessageHash) + } + GRDBStorage.shared.update(progress: 0.94, for: self, in: target) } - // Insert the data into GRDB + // MARK: - Insert into GRDB try autoreleasepool { + // MARK: --lastSnodePoolRefreshDate + db[.lastSnodePoolRefreshDate] = lastSnodePoolRefreshDate + // MARK: --SnodePool + try snodeResult.forEach { legacySnode in try Snode( address: legacySnode.address, @@ -92,6 +155,9 @@ enum _003_YDBToGRDBMigration: Migration { x25519PublicKey: legacySnode.publicKeySet.x25519Key ).insert(db) } + GRDBStorage.shared.update(progress: 0.96, for: self, in: target) + + // MARK: --SnodeSets try snodeSetResult.forEach { key, legacySnodeSet in try legacySnodeSet.enumerated().forEach { nodeIndex, legacySnode in @@ -104,34 +170,12 @@ enum _003_YDBToGRDBMigration: Migration { ).insert(db) } } + GRDBStorage.shared.update(progress: 0.98, for: self, in: target) } - // MARK: - Received Messages & Last Message Hash - - var lastMessageResults: [String: (hash: String, json: JSON)] = [:] - var receivedMessageResults: [String: Set] = [:] - - // TODO: Move into the top read block??? - Storage.read { transaction in - // Extract the received message hashes - transaction.enumerateKeysAndObjects(inCollection: SSKLegacy.receivedMessagesCollection) { key, object, _ in - guard let hashSet = object as? Set else { return } - receivedMessageResults[key] = hashSet - } - - // Retrieve the last message info - transaction.enumerateKeysAndObjects(inCollection: SSKLegacy.lastMessageHashCollection) { key, object, _ in - guard let lastMessageJson = object as? JSON else { return } - guard let lastMessageHash: String = lastMessageJson["hash"] as? String else { return } - - // Note: We remove the value from 'receivedMessageResults' as we want to try and use - // it's actual 'expirationDate' value - lastMessageResults[key] = (lastMessageHash, lastMessageJson) - receivedMessageResults[key] = receivedMessageResults[key]?.removing(lastMessageHash) - } - } - try autoreleasepool { + // MARK: --Received Messages + try receivedMessageResults.forEach { key, hashes in try hashes.forEach { hash in _ = try SnodeReceivedMessageInfo( @@ -141,6 +185,9 @@ enum _003_YDBToGRDBMigration: Migration { ).inserted(db) } } + GRDBStorage.shared.update(progress: 0.99, for: self, in: target) + + // MARK: --Last Message Hash try lastMessageResults.forEach { key, data in let expirationDateMs: Int64 = ((data.json["expirationDate"] as? Int64) ?? 0) diff --git a/SessionUtilitiesKit/Database/GRDBStorage.swift b/SessionUtilitiesKit/Database/GRDBStorage.swift index 305cdedf4..4009d7b3c 100644 --- a/SessionUtilitiesKit/Database/GRDBStorage.swift +++ b/SessionUtilitiesKit/Database/GRDBStorage.swift @@ -170,6 +170,7 @@ public final class GRDBStorage { self.migrator?.asyncMigrate(dbPool) { [weak self] _, error in self?.hasCompletedMigrations = true self?.migrationProgressUpdater = nil + SUKLegacy.clearLegacyDatabaseInstance() onComplete((error == nil), needsConfigSync) } diff --git a/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacy.swift b/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacy.swift index 1bd4f7da4..a691334df 100644 --- a/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacy.swift +++ b/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacy.swift @@ -1,8 +1,17 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import YapDatabase public enum SUKLegacy { + // MARK: - YapDatabase + + private static let keychainService = "TSKeyChainService" + private static let keychainDBCipherKeySpec = "OWSDatabaseCipherKeySpec" + private static let sqlCipherKeySpecLength = 48 + + private static var database: Atomic? + // MARK: - Collections and Keys internal static let userAccountRegisteredNumberKey = "TSStorageRegisteredNumberKey" @@ -14,6 +23,103 @@ public enum SUKLegacy { internal static let identityKeyStoreIdentityKey = "TSStorageManagerIdentityKeyStoreIdentityKey" internal static let identityKeyStoreCollection = "TSStorageManagerIdentityKeyStoreCollection" + // MARK: - Database Functions + + private static var legacyDatabaseFilepath: String { + let sharedDirUrl: URL = URL(fileURLWithPath: OWSFileSystem.appSharedDataDirectoryPath()) + + return sharedDirUrl + .appendingPathComponent("database") + .appendingPathComponent("Signal.sqlite") + .path + } + + private static let legacyDatabaseDeserializer: YapDatabaseDeserializer = { + return { (collection: String, key: String, data: Data) -> Any in + /// **Note:** The old `init(forReadingWith:)` method has been deprecated with `init(forReadingFrom:)` + /// and Apple changed the default of `requiresSecureCoding` to be true, this results in some of the types from failing + /// to decode, as a result we need to set it to false here + let unarchiver: NSKeyedUnarchiver? = try? NSKeyedUnarchiver(forReadingFrom: data) + unarchiver?.requiresSecureCoding = false + + guard !data.isEmpty, let result = unarchiver?.decodeObject(forKey: "root") else { + return UnknownDBObject() + } + + return result + } + }() + + public static var hasLegacyDatabaseFile: Bool { + return FileManager.default.fileExists(atPath: legacyDatabaseFilepath) + } + + @discardableResult public static func loadDatabaseIfNeeded() -> Bool { + guard SUKLegacy.database == nil else { return true } + + /// Ensure the databaseKeySpec exists + var maybeKeyData: Data? = try? CurrentAppContext().keychainStorage().data( + forService: keychainService, + key: keychainDBCipherKeySpec + ) + defer { if maybeKeyData != nil { maybeKeyData!.resetBytes(in: 0.. YapDatabaseConnection? { + SUKLegacy.loadDatabaseIfNeeded() + + return self.database?.wrappedValue.newConnection() + } + + public static func clearLegacyDatabaseInstance() { + self.database = nil + } + + // MARK: - UnknownDBObject + + @objc(LegacyUnknownDBObject) + public class UnknownDBObject: NSObject, NSCoding { + override public init() {} + public required init?(coder: NSCoder) {} + public func encode(with coder: NSCoder) { fatalError("Shouldn't be encoding this type") } + } + + // MARK: - LagacyKeyPair + @objc(LegacyKeyPair) public class KeyPair: NSObject, NSCoding { private static let keyLength: Int = 32 diff --git a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift index fd1392956..e1e19ea0c 100644 --- a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -4,9 +4,10 @@ import Foundation import GRDB enum _001_InitialSetupMigration: Migration { + static let target: TargetMigrations.Identifier = .utilitiesKit static let identifier: String = "initialSetup" - static let minExpectedRunDuration: TimeInterval = 0.1 static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 static func migrate(_ db: Database) throws { try db.create(table: Identity.self) { t in diff --git a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift index dd8e6b800..61fb1aed3 100644 --- a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -7,9 +7,10 @@ import Curve25519Kit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` enum _002_SetupStandardJobs: Migration { + static let target: TargetMigrations.Identifier = .utilitiesKit static let identifier: String = "SetupStandardJobs" - static let minExpectedRunDuration: TimeInterval = 0.1 static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 static func migrate(_ db: Database) throws { try autoreleasepool { diff --git a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift index a5b04d43e..d70216cc3 100644 --- a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -4,12 +4,18 @@ import Foundation import GRDB enum _003_YDBToGRDBMigration: Migration { + static let target: TargetMigrations.Identifier = .utilitiesKit static let identifier: String = "YDBToGRDBMigration" - static let minExpectedRunDuration: TimeInterval = 0.1 static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 static func migrate(_ db: Database) throws { - // MARK: - Identity keys + guard let dbConnection: YapDatabaseConnection = SUKLegacy.newDatabaseConnection() else { + SNLog("[Migration Warning] No legacy database, skipping \(target.key(with: self))") + return + } + + // MARK: - Read from Legacy Database // Note: Want to exclude the Snode's we already added from the 'onionRequestPathResult' var registeredNumber: String? @@ -24,7 +30,9 @@ enum _003_YDBToGRDBMigration: Migration { forClassName: "ECKeyPair" ) - Storage.read { transaction in + dbConnection.read { transaction in + // MARK: --Identity keys + registeredNumber = transaction.object( forKey: SUKLegacy.userAccountRegisteredNumberKey, inCollection: SUKLegacy.userAccountCollection @@ -73,9 +81,12 @@ enum _003_YDBToGRDBMigration: Migration { throw StorageError.migrationFailed } - print("RAWR publicKey \(userX25519KeyPair.publicKey.toHexString())") + + // MARK: - Insert into GRDB + try autoreleasepool { - // Insert the data into GRDB + // MARK: --Identity keys + try Identity( variant: .seed, data: Data(hex: seedHexString) diff --git a/SessionUtilitiesKit/Database/Types/Migration.swift b/SessionUtilitiesKit/Database/Types/Migration.swift index 69d27e754..b26bf1b97 100644 --- a/SessionUtilitiesKit/Database/Types/Migration.swift +++ b/SessionUtilitiesKit/Database/Types/Migration.swift @@ -4,9 +4,20 @@ import Foundation import GRDB public protocol Migration { + static var target: TargetMigrations.Identifier { get } static var identifier: String { get } static var needsConfigSync: Bool { get } static var minExpectedRunDuration: TimeInterval { get } static func migrate(_ db: Database) throws } + +public extension Migration { + static func loggedMigrate(_ targetIdentifier: TargetMigrations.Identifier) -> ((_ db: Database) throws -> ()) { + return { (db: Database) in + SNLog("[Migration Info] Starting \(targetIdentifier.key(with: self))") + try migrate(db) + SNLog("[Migration Info] Completed \(targetIdentifier.key(with: self))") + } + } +} diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index caf730300..25e67d4ec 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -866,7 +866,7 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet self.associateData = associateData } - convenience init( + public convenience init( trackedAgainst: Table.Type, observedChanges: [PagedData.ObservedChanges], dataQuery: @escaping (SQL?) -> SQLRequest, diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift index de793d071..8c3916b3a 100644 --- a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift +++ b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift @@ -27,7 +27,7 @@ public struct TargetMigrations: Comparable { return (lhsIndex < rhsIndex) } - func key(with migration: Migration.Type) -> String { + public func key(with migration: Migration.Type) -> String { return "\(self.rawValue).\(migration.identifier)" } } @@ -43,6 +43,10 @@ public struct TargetMigrations: Comparable { identifier: Identifier, migrations: [MigrationSet] ) { + guard !migrations.contains(where: { migration in migration.contains(where: { $0.target != identifier }) }) else { + preconditionFailure("Attempted to register a migration with the wrong target") + } + self.identifier = identifier self.migrations = migrations } diff --git a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift index ccc073f35..337dd805f 100644 --- a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift @@ -4,7 +4,10 @@ import Foundation import GRDB public extension DatabaseMigrator { - mutating func registerMigration(_ identifier: TargetMigrations.Identifier, migration: Migration.Type, foreignKeyChecks: ForeignKeyChecks = .deferred) { - self.registerMigration("\(identifier).\(migration.identifier)", migrate: migration.migrate) + mutating func registerMigration(_ targetIdentifier: TargetMigrations.Identifier, migration: Migration.Type, foreignKeyChecks: ForeignKeyChecks = .deferred) { + self.registerMigration( + targetIdentifier.key(with: migration), + migrate: migration.loggedMigrate(targetIdentifier) + ) } } diff --git a/SessionUtilitiesKit/General/NSArray+Functional.h b/SessionUtilitiesKit/General/NSArray+Functional.h deleted file mode 100644 index e8a293376..000000000 --- a/SessionUtilitiesKit/General/NSArray+Functional.h +++ /dev/null @@ -1,9 +0,0 @@ -#import - -@interface NSArray (Functional) - -- (BOOL)contains:(BOOL (^)(id))predicate; -- (NSArray *)filtered:(BOOL (^)(id))isIncluded; -- (NSArray *)map:(id (^)(id))transform; - -@end diff --git a/SessionUtilitiesKit/General/NSArray+Functional.m b/SessionUtilitiesKit/General/NSArray+Functional.m deleted file mode 100644 index 8e8e5a131..000000000 --- a/SessionUtilitiesKit/General/NSArray+Functional.m +++ /dev/null @@ -1,32 +0,0 @@ -#import "NSArray+Functional.h" - -@implementation NSArray (Functional) - -- (BOOL)contains:(BOOL (^)(id))predicate { - for (id object in self) { - BOOL isPredicateSatisfied = predicate(object); - if (isPredicateSatisfied) { return YES; } - } - return NO; -} - -- (NSArray *)filtered:(BOOL (^)(id))isIncluded { - NSMutableArray *result = [NSMutableArray new]; - for (id object in self) { - if (isIncluded(object)) { - [result addObject:object]; - } - } - return result; -} - -- (NSArray *)map:(id (^)(id))transform { - NSMutableArray *result = [NSMutableArray new]; - for (id object in self) { - id transformedObject = transform(object); - [result addObject:transformedObject]; - } - return result; -} - -@end diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 77598766a..4b29aa631 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -52,6 +52,7 @@ public final class JobRunner { let messageSendQueue: JobQueue = JobQueue( type: .messageSend, + executionType: .concurrent, // Allow as many jobs to run at once as supported by the device qos: .default, jobVariants: [ jobVariants.remove(.attachmentUpload), @@ -62,6 +63,7 @@ public final class JobRunner { ) let messageReceiveQueue: JobQueue = JobQueue( type: .messageReceive, + executionType: .concurrent, // Allow as many jobs to run at once as supported by the device qos: .default, jobVariants: [ jobVariants.remove(.messageReceive) @@ -320,33 +322,48 @@ private final class JobQueue { } } + fileprivate enum ExecutionType { + /// A serial queue will execute one job at a time until the queue is empty, then will load any new/deferred + /// jobs and run those one at a time + case serial + + /// A concurrent queue will execute as many jobs as the device supports at once until the queue is empty, + /// then will load any new/deferred jobs and try to start them all + case concurrent + } + private class Trigger { - private weak var queue: JobQueue? private var timer: Timer? + fileprivate var fireTimestamp: TimeInterval = 0 static func create(queue: JobQueue, timestamp: TimeInterval) -> Trigger? { - // Setup the trigger (wait at least 1 second before triggering) + /// Setup the trigger (wait at least 1 second before triggering) + /// + /// **Note:** We use the `Timer.scheduledTimerOnMainThread` method because running a timer + /// on our random queue threads results in the timer never firing, the `start` method will redirect itself to + /// the correct thread let trigger: Trigger = Trigger() - trigger.queue = queue - trigger.timer = Timer.scheduledTimer( - timeInterval: max(1, (timestamp - Date().timeIntervalSince1970)), - target: self, - selector: #selector(start), - userInfo: nil, - repeats: false + trigger.fireTimestamp = max(1, (timestamp - Date().timeIntervalSince1970)) + trigger.timer = Timer.scheduledTimerOnMainThread( + withTimeInterval: trigger.fireTimestamp, + repeats: false, + block: { [weak queue] _ in + queue?.start() + } ) return trigger } - deinit { timer?.invalidate() } - - @objc func start() { - queue?.start() + func invalidate() { + // Need to do this to prevent a strong reference cycle + timer?.invalidate() + timer = nil } } private let type: QueueType + private let executionType: ExecutionType private let qosClass: DispatchQoS private let queueKey: DispatchSpecificKey = DispatchSpecificKey() private let queueContext: String @@ -360,7 +377,7 @@ private final class JobQueue { let result: DispatchQueue = DispatchQueue( label: self.queueContext, qos: self.qosClass, - attributes: [], + attributes: (self.executionType == .concurrent ? [.concurrent] : []), autoreleaseFrequency: .inherit, target: nil ) @@ -378,8 +395,15 @@ private final class JobQueue { // MARK: - Initialization - init(type: QueueType, qos: DispatchQoS, jobVariants: [Job.Variant], onQueueDrained: (() -> ())? = nil) { + init( + type: QueueType, + executionType: ExecutionType = .serial, + qos: DispatchQoS, + jobVariants: [Job.Variant], + onQueueDrained: (() -> ())? = nil + ) { self.type = type + self.executionType = executionType self.queueContext = "JobQueue-\(type.name)" self.qosClass = qos self.jobVariants = jobVariants @@ -483,10 +507,10 @@ private final class JobQueue { // MARK: - Job Running - fileprivate func start() { + fileprivate func start(force: Bool = false) { // We only want the JobRunner to run in the main app guard CurrentAppContext().isMainApp else { return } - guard !isRunning.wrappedValue else { return } + guard force || !isRunning.wrappedValue else { return } // The JobRunner runs synchronously we need to ensure this doesn't start // on the main thread (if it is on the main thread then swap to a different thread) @@ -497,9 +521,21 @@ private final class JobQueue { return } + // Flag the JobRunner as running (to prevent something else from trying to start it + // and messing with the execution behaviour) + var wasAlreadyRunning: Bool = false + isRunning.mutate { isRunning in + wasAlreadyRunning = isRunning + isRunning = true + } + // Get any pending jobs + let jobIdsAlreadyRunning: Set = jobsCurrentlyRunning.wrappedValue + let jobsAlreadyInQueue: Set = queue.wrappedValue.compactMap { $0.id }.asSet() let jobsToRun: [Job] = GRDBStorage.shared.read { db in try Job.filterPendingJobs(variants: jobVariants) + .filter(!jobIdsAlreadyRunning.contains(Job.Columns.id)) // Exclude jobs already running + .filter(!jobsAlreadyInQueue.contains(Job.Columns.id)) // Exclude jobs already in the queue .fetchAll(db) } .defaulting(to: []) @@ -508,28 +544,24 @@ private final class JobQueue { var jobCount: Int = 0 queue.mutate { queue in - // Avoid re-adding jobs to the queue that are already in it - let jobsNotAlreadyInQueue: [Job] = jobsToRun - .filter { job in !queue.contains(where: { $0.id == job.id }) } - - // Add the jobs to the queue - if !jobsNotAlreadyInQueue.isEmpty { - queue.append(contentsOf: jobsToRun) - } - + queue.append(contentsOf: jobsToRun) jobCount = queue.count } - // If there are no pending jobs then schedule the JobRunner to start again - // when the next scheduled job should start + // If there are no pending jobs and nothing in the queue then schedule the JobRunner + // to start again when the next scheduled job should start guard jobCount > 0 else { - isRunning.mutate { $0 = false } - scheduleNextSoonestJob() + if jobIdsAlreadyRunning.isEmpty { + isRunning.mutate { $0 = false } + scheduleNextSoonestJob() + } return } // Run the first job in the queue - SNLog("[JobRunner] Starting \(queueContext) with (\(jobCount) job\(jobCount != 1 ? "s" : ""))") + if !wasAlreadyRunning { + SNLog("[JobRunner] Starting \(queueContext) with (\(jobCount) job\(jobCount != 1 ? "s" : ""))") + } runNextJob() } @@ -542,7 +574,13 @@ private final class JobQueue { return } guard let (nextJob, numJobsRemaining): (Job, Int) = queue.mutate({ queue in queue.popFirst().map { ($0, queue.count) } }) else { - isRunning.mutate { $0 = false } + // If it's a serial queue, or there are no more jobs running then update the 'isRunning' flag + if executionType != .concurrent || jobsCurrentlyRunning.wrappedValue.isEmpty { + isRunning.mutate { $0 = false } + } + + // Always attempt to schedule the next soonest job (otherwise if enough jobs get started in rapid + // succession then pending/failed jobs in the database may never get re-started in a concurrent queue) scheduleNextSoonestJob() return } @@ -606,15 +644,20 @@ private final class JobQueue { return } - // Otherwise re-add the current job after it's dependencies - queue.mutate { queue in - guard let lastDependencyIndex: Int = queue.lastIndex(where: { jobDependencyIds.contains($0.id ?? -1) }) else { - queue.append(nextJob) - return + // Otherwise re-add the current job after it's dependencies (if this isn't a concurrent + // queue - don't want to immediately try to start the job again only for it to end up back + // in here) + if executionType != .concurrent { + queue.mutate { queue in + guard let lastDependencyIndex: Int = queue.lastIndex(where: { jobDependencyIds.contains($0.id ?? -1) }) else { + queue.append(nextJob) + return + } + + queue.insert(nextJob, at: lastDependencyIndex + 1) } - - queue.insert(nextJob, at: lastDependencyIndex + 1) } + handleJobDeferred(nextJob) return } @@ -624,10 +667,17 @@ private final class JobQueue { // Note: We need to store 'numJobsRemaining' in it's own variable because // the 'SNLog' seems to dispatch to it's own queue which ends up getting // blocked by the JobRunner's queue becuase 'jobQueue' is Atomic - nextTrigger.mutate { $0 = nil } + var numJobsRunning: Int = 0 + nextTrigger.mutate { trigger in + trigger?.invalidate() // Need to invalidate to prevent a memory leak + trigger = nil + } isRunning.mutate { $0 = true } - jobsCurrentlyRunning.mutate { $0 = $0.inserting(nextJob.id) } - SNLog("[JobRunner] \(queueContext) started job (\(numJobsRemaining) remaining)") + jobsCurrentlyRunning.mutate { jobsCurrentlyRunning in + jobsCurrentlyRunning = jobsCurrentlyRunning.inserting(nextJob.id) + numJobsRunning = jobsCurrentlyRunning.count + } + SNLog("[JobRunner] \(queueContext) started job (\(executionType == .concurrent ? "\(numJobsRunning) currently running, " : "")\(numJobsRemaining) remaining)") jobExecutor.run( nextJob, @@ -635,6 +685,14 @@ private final class JobQueue { failure: handleJobFailed, deferred: handleJobDeferred ) + + // If this queue executes concurrently and there are still jobs remaining then immediately attempt + // to start the next job + if executionType == .concurrent && numJobsRemaining > 0 { + internalQueue.async { [weak self] in + self?.runNextJob() + } + } } private func scheduleNextSoonestJob() { @@ -647,7 +705,9 @@ private final class JobQueue { // If there are no remaining jobs the trigger the 'onQueueDrained' callback and stop guard let nextJobTimestamp: TimeInterval = nextJobTimestamp else { - self.onQueueDrained?() + if executionType != .concurrent || jobsCurrentlyRunning.wrappedValue.isEmpty { + self.onQueueDrained?() + } return } @@ -655,17 +715,33 @@ private final class JobQueue { let secondsUntilNextJob: TimeInterval = (nextJobTimestamp - Date().timeIntervalSince1970) guard secondsUntilNextJob > 0 else { - SNLog("[JobRunner] Restarting \(queueContext) immediately for job scheduled \(Int(ceil(abs(secondsUntilNextJob)))) second\(Int(ceil(abs(secondsUntilNextJob))) == 1 ? "" : "s")) ago") + // Only log that the queue is getting restarted if this queue had actually been about to stop + if executionType != .concurrent || jobsCurrentlyRunning.wrappedValue.isEmpty { + let timingString: String = (nextJobTimestamp == 0 ? + "that should be in the queue" : + "scheduled \(Int(ceil(abs(secondsUntilNextJob)))) second\(Int(ceil(abs(secondsUntilNextJob))) == 1 ? "" : "s") ago" + ) + SNLog("[JobRunner] Restarting \(queueContext) immediately for job \(timingString)") + } + // Trigger the 'start' function to load in any pending jobs that aren't already in the + // queue (for concurrent queues we want to force them to load in pending jobs and add + // them to the queue regardless of whether the queue is already running) internalQueue.async { [weak self] in - self?.start() + self?.start(force: (self?.executionType == .concurrent)) } return } + // Only schedule a trigger if this queue has actually completed + guard executionType != .concurrent || jobsCurrentlyRunning.wrappedValue.isEmpty else { return } + // Setup a trigger - SNLog("[JobRunner] Stopping \(queueContext) until next job in \(Int(ceil(abs(secondsUntilNextJob)))) second\(Int(ceil(abs(secondsUntilNextJob))) == 1 ? "" : "s"))") - nextTrigger.mutate { $0 = Trigger.create(queue: self, timestamp: nextJobTimestamp) } + SNLog("[JobRunner] Stopping \(queueContext) until next job in \(Int(ceil(abs(secondsUntilNextJob)))) second\(Int(ceil(abs(secondsUntilNextJob))) == 1 ? "" : "s")") + nextTrigger.mutate { trigger in + trigger?.invalidate() // Need to invalidate the old trigger to prevent a memory leak + trigger = Trigger.create(queue: self, timestamp: nextJobTimestamp) + } } // MARK: - Handling Results @@ -708,6 +784,29 @@ private final class JobQueue { default: break } + // For concurrent queues retrieve any 'dependant' jobs and re-add them here (if they have other + // dependencies they will be removed again when they try to execute) + if executionType == .concurrent { + let dependantJobs: [Job] = GRDBStorage.shared + .read { db in try job.dependantJobs.fetchAll(db) } + .defaulting(to: []) + let dependantJobIds: [Int64] = dependantJobs + .compactMap { $0.id } + let jobIdsNotInQueue: Set = dependantJobIds + .asSet() + .subtracting(queue.wrappedValue.compactMap { $0.id }) + + // If there are dependant jobs which aren't in the queue we should just append them + if !jobIdsNotInQueue.isEmpty { + queue.mutate { queue in + queue.append( + contentsOf: dependantJobs + .filter { jobIdsNotInQueue.contains($0.id ?? -1) } + ) + } + } + } + // The job is removed from the queue before it runs so all we need to to is remove it // from the 'currentlyRunning' set and start the next one jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } @@ -732,6 +831,7 @@ private final class JobQueue { // If this is the blocking queue and a "blocking" job failed then rerun it immediately if self.type == .blocking && job.shouldBlockFirstRunEachSession { SNLog("[JobRunner] \(queueContext) \(job.variant) job failed; retrying immediately") + jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } queue.mutate { $0.insert(job, at: 0) } internalQueue.async { [weak self] in @@ -752,9 +852,24 @@ private final class JobQueue { else { SNLog("[JobRunner] \(queueContext) \(job.variant) failed permanently\(maxFailureCount >= 0 ? "; too many retries" : "")") + let dependantJobIds: [Int64] = try job.dependantJobs + .select(.id) + .asRequest(of: Int64.self) + .fetchAll(db) + // If the job permanently failed or we have performed all of our retry attempts - // then delete the job (it'll probably never succeed) + // then delete the job and all of it's dependant jobs (it'll probably never succeed) + _ = try job.dependantJobs + .deleteAll(db) + _ = try job.delete(db) + + // Remove the dependant jobs from the queue (so we don't try to run a deleted job) + if !dependantJobIds.isEmpty { + queue.mutate { queue in + queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) } + } + } return } @@ -783,7 +898,7 @@ private final class JobQueue { .fetchAll(db) // Remove the dependant jobs from the queue (so we don't get stuck in a loop of trying - // to run dependecies indefinitely + // to run dependecies indefinitely) if !dependantJobIds.isEmpty { queue.mutate { queue in queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) } diff --git a/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h b/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h index 02e57fb9a..fc889a5b2 100644 --- a/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h +++ b/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h @@ -7,7 +7,6 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[]; #import #import #import -#import #import #import #import diff --git a/SignalUtilitiesKit/Database/Migrations/BlockingManagerRemovalMigration.swift b/SignalUtilitiesKit/Database/Migrations/BlockingManagerRemovalMigration.swift deleted file mode 100644 index 1e2ead55e..000000000 --- a/SignalUtilitiesKit/Database/Migrations/BlockingManagerRemovalMigration.swift +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import YapDatabase -import SessionMessagingKit - -@objc(SNBlockingManagerRemovalMigration) -public class BlockingManagerRemovalMigration: OWSDatabaseMigration { - @objc - class func migrationId() -> String { - return "004" - } - - override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) { - self.doMigrationAsync(completion: completion) - } - - private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) { - // These are the legacy keys that were used to persist the "block list" state - let kOWSBlockingManager_BlockListCollection: String = "kOWSBlockingManager_BlockedPhoneNumbersCollection" - let kOWSBlockingManager_BlockedPhoneNumbersKey: String = "kOWSBlockingManager_BlockedPhoneNumbersKey" - - // Note: These will be done in the YDB to GRDB migration but have added it here to be safe - NSKeyedUnarchiver.setClass( - SMKLegacy._Contact.self, - forClassName: "SNContact" - ) - - let dbConnection: YapDatabaseConnection = primaryStorage.newDatabaseConnection() - - let blockedSessionIds: Set = Set(dbConnection.object( - forKey: kOWSBlockingManager_BlockedPhoneNumbersKey, - inCollection: kOWSBlockingManager_BlockListCollection - ) as? [String] ?? []) - - Storage.write( - with: { transaction in - var result: Set = [] - - transaction.enumerateRows(inCollection: SMKLegacy.contactCollection) { _, object, _, _ in - guard let contact = object as? SMKLegacy._Contact else { return } - result.insert(contact) - } - - result - .filter { contact -> Bool in blockedSessionIds.contains(contact.sessionID) } - .forEach { contact in - contact.isBlocked = true - transaction.setObject(contact, forKey: contact.sessionID, inCollection: SMKLegacy.contactCollection) - } - - // Now that the values have been migrated we can clear out the old collection - transaction.removeAllObjects(inCollection: kOWSBlockingManager_BlockListCollection) - - self.save(with: transaction) // Intentionally capture self - }, - completion: { - completion(true, true) - } - ) - } -} diff --git a/SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift b/SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift deleted file mode 100644 index b2290d10b..000000000 --- a/SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import YapDatabase -import SessionMessagingKit - -@objc(SNContactsMigration) -public class ContactsMigration: OWSDatabaseMigration { - - @objc - class func migrationId() -> String { - return "001" - } - - override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) { - self.doMigrationAsync(completion: completion) - } - - private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) { - var contacts: [SMKLegacy._Contact] = [] - - // Note: These will be done in the YDB to GRDB migration but have added it here to be safe - NSKeyedUnarchiver.setClass( - SMKLegacy._Thread.self, - forClassName: "TSThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ContactThread.self, - forClassName: "TSContactThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._Contact.self, - forClassName: "SNContact" - ) - - Storage.read { transaction in - transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.threadCollection) { _, object, _ in - guard let thread = object as? SMKLegacy._ContactThread else { return } - - let sessionId: String = SMKLegacy._ContactThread.contactSessionId(fromThreadId: thread.uniqueId) - let contact: SMKLegacy._Contact? = transaction.object(forKey: sessionId, inCollection: SMKLegacy.contactCollection) as? SMKLegacy._Contact - - contact?.isTrusted = true - contacts = contacts.appending(contact) - } - } - - Storage.write(with: { transaction in - contacts.forEach { contact in - transaction.setObject(contact, forKey: contact.sessionID, inCollection: SMKLegacy.contactCollection) - } - self.save(with: transaction) // Intentionally capture self - }, completion: { - completion(true, false) - }) - } -} diff --git a/SignalUtilitiesKit/Database/Migrations/MessageRequestsMigration.swift b/SignalUtilitiesKit/Database/Migrations/MessageRequestsMigration.swift deleted file mode 100644 index 92223ee70..000000000 --- a/SignalUtilitiesKit/Database/Migrations/MessageRequestsMigration.swift +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import YapDatabase -import SessionMessagingKit - -@objc(SNMessageRequestsMigration) -public class MessageRequestsMigration: OWSDatabaseMigration { - - @objc - class func migrationId() -> String { - return "002" - } - - override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) { - self.doMigrationAsync(completion: completion) - } - - private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) { - let userPublicKey: String = getUserHexEncodedPublicKey() - var contacts: Set = Set() - var threads: [SMKLegacy._Thread] = [] - - // Note: These will be done in the YDB to GRDB migration but have added it here to be safe - NSKeyedUnarchiver.setClass( - SMKLegacy._Thread.self, - forClassName: "TSThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ContactThread.self, - forClassName: "TSContactThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._GroupThread.self, - forClassName: "TSGroupThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._GroupModel.self, - forClassName: "TSGroupModel" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._Contact.self, - forClassName: "SNContact" - ) - - Storage.read { transaction in - transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.threadCollection) { _, object, _ in - guard let thread: SMKLegacy._Thread = object as? SMKLegacy._Thread else { return } - - if thread is SMKLegacy._ContactThread { - let sessionId: String = SMKLegacy._ContactThread.contactSessionId(fromThreadId: thread.uniqueId) - - if let contact: SMKLegacy._Contact = transaction.object(forKey: sessionId, inCollection: SMKLegacy.contactCollection) as? SMKLegacy._Contact { - contact.isApproved = true - contact.didApproveMe = true - contacts.insert(contact) - } - } - else if let groupThread: SMKLegacy._GroupThread = thread as? SMKLegacy._GroupThread, groupThread.isClosedGroup { - let groupAdmins: [String] = groupThread.groupModel.groupAdminIds - - groupAdmins.forEach { sessionId in - if let contact: SMKLegacy._Contact = transaction.object(forKey: sessionId, inCollection: SMKLegacy.contactCollection) as? SMKLegacy._Contact { - contact.isApproved = true - contact.didApproveMe = true - contacts.insert(contact) - } - } - } - - threads.append(thread) - } - - if let user = transaction.object(forKey: userPublicKey, inCollection: SMKLegacy.contactCollection) as? SMKLegacy._Contact { - user.isApproved = true - user.didApproveMe = true - contacts.insert(user) - } - } - - Storage.write(with: { transaction in - contacts.forEach { contact in - transaction.setObject(contact, forKey: contact.sessionID, inCollection: SMKLegacy.contactCollection) - } - threads.forEach { thread in - transaction.setObject(thread, forKey: thread.uniqueId, inCollection: SMKLegacy.threadCollection) - } - self.save(with: transaction) // Intentionally capture self - }, completion: { - completion(true, true) - }) - } -} diff --git a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.h b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.h deleted file mode 100644 index d99decd7e..000000000 --- a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^OWSDatabaseMigrationCompletion)(BOOL success, BOOL requiresConfigurationSync); - -@class OWSPrimaryStorage; - -@interface OWSDatabaseMigration : TSYapDatabaseObject - -@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; - -// Prefer nonblocking (async) migrations by overriding `runUpWithTransaction:` in a subclass. -// Blocking migrations running too long will crash the app, effectively bricking install -// because the user will never get past it. -// If you must write a launch-blocking migration, override runUp. -- (void)runUpWithCompletion:(OWSDatabaseMigrationCompletion)completion; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.m b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.m deleted file mode 100644 index 53ba11003..000000000 --- a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigration.m +++ /dev/null @@ -1,109 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSDatabaseMigration.h" -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSDatabaseMigration - -#pragma mark - Dependencies - -- (OWSPrimaryStorage *)primaryStorage -{ - OWSAssertDebug(SSKEnvironment.shared.primaryStorage); - - return SSKEnvironment.shared.primaryStorage; -} - -#pragma mark - - -- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction -{ - OWSLogInfo(@"marking migration as complete."); - - [super saveWithTransaction:transaction]; -} - -- (instancetype)init -{ - self = [super initWithUniqueId:[self.class migrationId]]; - if (!self) { - return self; - } - - return self; -} - -+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey -{ - if ([propertyKey isEqualToString:@"primaryStorage"]) { - return MTLPropertyStorageNone; - } else { - return [super storageBehaviorForPropertyWithKey:propertyKey]; - } -} - -+ (NSString *)migrationId -{ - OWSAbstractMethod(); - return @""; -} - -+ (NSString *)collection -{ - // We want all subclasses in the same collection - return @"OWSDatabaseMigration"; -} - -- (void)runUpWithTransaction:(YapDatabaseReadWriteTransaction *)transaction -{ - OWSAbstractMethod(); -} - -- (void)runUpWithCompletion:(OWSDatabaseMigrationCompletion)completion -{ - OWSAssertDebug(completion); - - [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [self runUpWithTransaction:transaction]; - } - completion:^{ - OWSLogInfo(@"Completed migration %@", self.uniqueId); - [self save]; - - completion(true, false); - }]; -} - -#pragma mark - Database Connections - -#ifdef DEBUG -+ (YapDatabaseConnection *)dbReadConnection -{ - return self.dbReadWriteConnection; -} - -+ (YapDatabaseConnection *)dbReadWriteConnection -{ - return SSKEnvironment.shared.migrationDBConnection; -} - -- (YapDatabaseConnection *)dbReadConnection -{ - return OWSDatabaseMigration.dbReadConnection; -} - -- (YapDatabaseConnection *)dbReadWriteConnection -{ - return OWSDatabaseMigration.dbReadWriteConnection; -} -#endif - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.h b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.h deleted file mode 100644 index 58c75ec8c..000000000 --- a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^OWSDatabaseMigrationCompletion)(BOOL success, BOOL requiresConfigurationSync); - -@interface OWSDatabaseMigrationRunner : NSObject - -/** - * Run any outstanding version migrations. - */ -- (void)runAllOutstandingWithCompletion:(OWSDatabaseMigrationCompletion)completion; - -/** - * On new installations, no need to migrate anything. - */ -- (void)assumeAllExistingMigrationsRun; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m deleted file mode 100644 index 559c8dc03..000000000 --- a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m +++ /dev/null @@ -1,124 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "OWSDatabaseMigrationRunner.h" -#import "OWSDatabaseMigration.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSDatabaseMigrationRunner - -#pragma mark - Dependencies - -- (OWSPrimaryStorage *)primaryStorage -{ - OWSAssertDebug(SSKEnvironment.shared.primaryStorage); - - return SSKEnvironment.shared.primaryStorage; -} - -#pragma mark - - -// This should all migrations which do NOT qualify as safeBlockingMigrations: -- (NSArray *)allMigrations -{ - return @[ - [SNOpenGroupServerIdLookupMigration new], - [SNMessageRequestsMigration new], - [SNContactsMigration new], - [SNBlockingManagerRemovalMigration new] - ]; -} - -- (void)assumeAllExistingMigrationsRun -{ - for (OWSDatabaseMigration *migration in self.allMigrations) { - OWSLogInfo(@"Skipping migration on new install: %@", migration); - [migration save]; - } -} - -- (void)runAllOutstandingWithCompletion:(OWSDatabaseMigrationCompletion)completion -{ - [self removeUnknownMigrations]; - - [self runMigrations:[self.allMigrations mutableCopy] - prevWasSuccessful: true - prevNeedsConfigSync:false - completion:completion]; -} - -// Some users (especially internal users) will move back and forth between -// app versions. Whenever they move "forward" in the version history, we -// want them to re-run any new migrations. Therefore, when they move "backward" -// in the version history, we cull any unknown migrations. -- (void)removeUnknownMigrations -{ - NSMutableSet *knownMigrationIds = [NSMutableSet new]; - for (OWSDatabaseMigration *migration in self.allMigrations) { - [knownMigrationIds addObject:migration.uniqueId]; - } - - [OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - NSArray *savedMigrationIds = [transaction allKeysInCollection:OWSDatabaseMigration.collection]; - - NSMutableSet *unknownMigrationIds = [NSMutableSet new]; - [unknownMigrationIds addObjectsFromArray:savedMigrationIds]; - [unknownMigrationIds minusSet:knownMigrationIds]; - - for (NSString *unknownMigrationId in unknownMigrationIds) { - OWSLogInfo(@"Culling unknown migration: %@", unknownMigrationId); - [transaction removeObjectForKey:unknownMigrationId inCollection:OWSDatabaseMigration.collection]; - } - }]; -} - -// Run migrations serially to: -// -// * Ensure predictable ordering. -// * Prevent them from interfering with each other (e.g. deadlock). -- (void)runMigrations:(NSMutableArray *)migrations - prevWasSuccessful:(BOOL)prevWasSuccessful - prevNeedsConfigSync:(BOOL)prevNeedsConfigSync - completion:(OWSDatabaseMigrationCompletion)completion -{ - OWSAssertDebug(migrations); - OWSAssertDebug(completion); - - // If there are no more migrations to run, complete. - if (migrations.count < 1) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(prevWasSuccessful, prevNeedsConfigSync); - }); - return; - } - - // Pop next migration from front of queue. - OWSDatabaseMigration *migration = migrations.firstObject; - [migrations removeObjectAtIndex:0]; - - // If migration has already been run, skip it. - if ([OWSDatabaseMigration fetchObjectWithUniqueID:migration.uniqueId] != nil) { - [self runMigrations:migrations - prevWasSuccessful:prevWasSuccessful - prevNeedsConfigSync:prevNeedsConfigSync - completion:completion]; - return; - } - - OWSLogInfo(@"Running migration: %@", migration); - [migration runUpWithCompletion:^(BOOL successful, BOOL needsConfigSync){ - OWSLogInfo(@"Migration complete: %@", migration); - [self runMigrations:migrations - prevWasSuccessful:(prevWasSuccessful && successful) - prevNeedsConfigSync:(prevNeedsConfigSync || needsConfigSync) - completion:completion]; - }]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.h b/SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.h deleted file mode 100644 index d7c793e1b..000000000 --- a/SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -typedef BOOL (^DBRecordFilterBlock)(id record); - -@class YapDatabaseConnection; - -// Base class for migrations that resave all or a subset of -// records in a database collection. -@interface OWSResaveCollectionDBMigration : OWSDatabaseMigration - -- (void)resaveDBCollection:(NSString *)collection - filter:(nullable DBRecordFilterBlock)filter - dbConnection:(YapDatabaseConnection *)dbConnection - completion:(OWSDatabaseMigrationCompletion)completion; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.m b/SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.m deleted file mode 100644 index f899bed9b..000000000 --- a/SignalUtilitiesKit/Database/Migrations/OWSResaveCollectionDBMigration.m +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSResaveCollectionDBMigration.h" -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSResaveCollectionDBMigration - -- (void)resaveDBCollection:(NSString *)collection - filter:(nullable DBRecordFilterBlock)filter - dbConnection:(YapDatabaseConnection *)dbConnection - completion:(OWSDatabaseMigrationCompletion)completion -{ - OWSAssertDebug(collection.length > 0); - OWSAssertDebug(dbConnection); - OWSAssertDebug(completion); - - NSMutableArray *recordIds = [NSMutableArray new]; - [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [recordIds addObjectsFromArray:[transaction allKeysInCollection:collection]]; - OWSLogInfo(@"Migrating %lu records from: %@.", (unsigned long)recordIds.count, collection); - } - completion:^{ - [self resaveBatch:recordIds - collection:collection - filter:filter - dbConnection:dbConnection - completion:completion]; - }]; -} - -- (void)resaveBatch:(NSMutableArray *)recordIds - collection:(NSString *)collection - filter:(nullable DBRecordFilterBlock)filter - dbConnection:(YapDatabaseConnection *)dbConnection - completion:(OWSDatabaseMigrationCompletion)completion -{ - OWSAssertDebug(recordIds); - OWSAssertDebug(collection.length > 0); - OWSAssertDebug(dbConnection); - OWSAssertDebug(completion); - - OWSLogVerbose(@"%lu", (unsigned long)recordIds.count); - - if (recordIds.count < 1) { - completion(true, false); - return; - } - - [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - const int kBatchSize = 1000; - for (int i = 0; i < kBatchSize && recordIds.count > 0; i++) { - NSString *messageId = [recordIds lastObject]; - [recordIds removeLastObject]; - id record = [transaction objectForKey:messageId inCollection:collection]; - if (filter && !filter(record)) { - continue; - } - TSYapDatabaseObject *entity = (TSYapDatabaseObject *)record; - [entity saveWithTransaction:transaction]; - } - } - completion:^{ - // Process the next batch. - [self resaveBatch:recordIds - collection:collection - filter:filter - dbConnection:dbConnection - completion:completion]; - }]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift b/SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift deleted file mode 100644 index 54f7f0e83..000000000 --- a/SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import YapDatabase -import SessionMessagingKit - -@objc(SNOpenGroupServerIdLookupMigration) -public class OpenGroupServerIdLookupMigration: OWSDatabaseMigration { - @objc - class func migrationId() -> String { - return "003" - } - - override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) { - self.doMigrationAsync(completion: completion) - } - - private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) { - var lookups: [OpenGroupServerIdLookup] = [] - - // Note: These will be done in the YDB to GRDB migration but have added it here to be safe - NSKeyedUnarchiver.setClass( - SMKLegacy._Thread.self, - forClassName: "TSThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._ContactThread.self, - forClassName: "TSContactThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._GroupThread.self, - forClassName: "TSGroupThread" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._GroupModel.self, - forClassName: "TSGroupModel" - ) - // TODO: Add, SMKLegacy._OpenGroup, SMKLegacy._TSMessage (and related) - - Storage.write(with: { transaction in - transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.threadCollection) { _, object, _ in - guard let thread = object as? SMKLegacy._GroupThread else { return } - guard let openGroup: OpenGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId) else { return } - guard let interactionsByThread: YapDatabaseViewTransaction = transaction.ext(SMKLegacy.messageDatabaseViewExtensionName) as? YapDatabaseViewTransaction else { - return - } - - interactionsByThread.enumerateKeysAndObjects(inGroup: thread.uniqueId) { _, _, object, _, _ in - guard let tsMessage: TSMessage = object as? TSMessage else { return } - guard let tsMessageId: String = tsMessage.uniqueId else { return } - - lookups.append( - OpenGroupServerIdLookup( - server: openGroup.server, - room: openGroup.room, - serverId: tsMessage.openGroupServerMessageID, - tsMessageId: tsMessageId - ) - ) - } - } - - lookups.forEach { lookup in - Storage.shared.addOpenGroupServerIdLookup(lookup, using: transaction) - } - self.save(with: transaction) // Intentionally capture self - }, completion: { - completion(true, false) - }) - } -} diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 552e6569c..87593e7c3 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -769,11 +769,9 @@ extension SignalAttachmentItem: GalleryRailItem { func buildRailItemView() -> UIView { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill - - getThumbnailImage().map { image in - imageView.image = image - }.retainUntilComplete() - + imageView.backgroundColor = UIColor.black.withAlphaComponent(0.33) + imageView.image = getThumbnailImage() + return imageView } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift index dcb63474f..503b28ad0 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift @@ -61,22 +61,8 @@ class SignalAttachmentItem: Hashable { return attachment.captionText } - var imageSize: CGSize = .zero - - func getThumbnailImage() -> Promise { - return DispatchQueue.global().async(.promise) { () -> UIImage in - guard let image = self.attachment.staticThumbnail() else { - throw SignalAttachmentItemError.noThumbnail - } - return image - }.tap { result in - switch result { - case .fulfilled(let image): - self.imageSize = image.size - case .rejected(let error): - owsFailDebug("failed with error: \(error)") - } - } + func getThumbnailImage() -> UIImage? { + return attachment.staticThumbnail() } // MARK: Hashable diff --git a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h index e127a8286..7863687e9 100644 --- a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h +++ b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h @@ -7,18 +7,11 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[]; @import SessionSnodeKit; @import SessionUtilitiesKit; -#import #import #import #import -#import #import -#import -#import #import -#import -#import -#import #import #import #import @@ -35,4 +28,3 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[]; #import #import #import -#import diff --git a/SignalUtilitiesKit/Shared Views/GalleryRailView.swift b/SignalUtilitiesKit/Shared Views/GalleryRailView.swift index 74d001b1a..a4f585135 100644 --- a/SignalUtilitiesKit/Shared Views/GalleryRailView.swift +++ b/SignalUtilitiesKit/Shared Views/GalleryRailView.swift @@ -129,9 +129,12 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate { addSubview(scrollView) scrollView.autoPinEdgesToSuperviewMargins() - scrollView.addSubview(stackView) + scrollView.addSubview(stackClippingView) + stackClippingView.addSubview(stackView) + + stackClippingView.autoPinEdgesToSuperviewEdges() + stackClippingView.autoMatch(.height, to: .height, of: scrollView) stackView.autoPinEdgesToSuperviewEdges() - stackView.autoMatch(.height, to: .height, of: scrollView) } public required init?(coder aDecoder: NSCoder) { @@ -145,6 +148,14 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate { result.clipsToBounds = false result.layoutMargins = .zero result.isScrollEnabled = true + result.scrollIndicatorInsets = UIEdgeInsets(top: 0, leading: 0, bottom: -10, trailing: 0) + + return result + }() + + private let stackClippingView: UIView = { + let result: UIView = UIView() + result.clipsToBounds = true return result }() @@ -194,7 +205,7 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate { completion: { [weak self] _ in self?.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } self?.stackView.frame = oldFrame - self?.isHidden = true + self?.stackClippingView.isHidden = true self?.cellViews = [] } ) @@ -202,54 +213,74 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate { } // Otherwise slide it away, recreate it and then slide it back - var oldFrame: CGRect = self.stackView.frame let newCellViews: [GalleryRailCellView] = buildCellViews( items: album, cellViewBuilder: cellViewBuilder ) - - UIView.animate( - withDuration: (animationDuration / 2), - delay: 0, - options: .curveEaseIn, - animations: { [weak self] in - self?.stackView.frame = oldFrame.offsetBy( - dx: 0, - dy: oldFrame.height - ) - }, - completion: { [weak self] _ in + + let animateOut: ((CGRect, @escaping (CGRect) -> CGRect, @escaping (CGRect) -> ()) -> ()) = { [weak self] oldFrame, layoutNewItems, animateIn in + UIView.animate( + withDuration: (animationDuration / 2), + delay: 0, + options: .curveEaseIn, + animations: { + self?.stackView.frame = oldFrame.offsetBy( + dx: 0, + dy: oldFrame.height + ) + }, + completion: { _ in + let updatedOldFrame: CGRect = layoutNewItems(oldFrame) + animateIn(updatedOldFrame) + } + ) + } + let layoutNewItems: (CGRect) -> CGRect = { [weak self] oldFrame -> CGRect in + var updatedOldFrame: CGRect = oldFrame + + // Update the UI (need to re-offset it as the position gets reset during + // during these changes) + UIView.performWithoutAnimation { self?.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } newCellViews.forEach { cellView in self?.stackView.addArrangedSubview(cellView) } self?.cellViews = newCellViews - // Update the UI (need to re-offset it as the position gets reset during - // during these changes) - UIView.performWithoutAnimation { - self?.updateFocusedItem(focusedItem) - self?.isHidden = false - - oldFrame = (self?.stackView.frame) - .defaulting(to: oldFrame) - self?.stackView.frame = oldFrame.offsetBy( - dx: 0, - dy: oldFrame.height - ) - } + self?.updateFocusedItem(focusedItem) + self?.stackView.layoutIfNeeded() + self?.stackClippingView.isHidden = false - UIView.animate( - withDuration: (animationDuration / 2), - delay: 0, - options: .curveEaseOut, - animations: { [weak self] in - self?.stackView.frame = oldFrame - }, - completion: nil + updatedOldFrame = (self?.stackView.frame) + .defaulting(to: oldFrame) + self?.stackView.frame = oldFrame.offsetBy( + dx: 0, + dy: oldFrame.height ) } - ) + + return updatedOldFrame + } + let animateIn: (CGRect) -> () = { [weak self] oldFrame in + UIView.animate( + withDuration: (animationDuration / 2), + delay: 0, + options: .curveEaseOut, + animations: { [weak self] in + self?.stackView.frame = oldFrame + }, + completion: nil + ) + } + + // If we don't have arranged subviews already we can skip the 'animateOut' + guard !self.stackView.arrangedSubviews.isEmpty else { + let updatedOldFrame: CGRect = layoutNewItems(self.stackView.frame) + animateIn(updatedOldFrame) + return + } + + animateOut(self.stackView.frame, layoutNewItems, animateIn) } // MARK: - GalleryRailCellViewDelegate diff --git a/SignalUtilitiesKit/Utilities/NSArray+OWS.h b/SignalUtilitiesKit/Utilities/NSArray+OWS.h deleted file mode 100644 index 17d2b34e2..000000000 --- a/SignalUtilitiesKit/Utilities/NSArray+OWS.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface NSArray (OWS) - -- (NSArray *)uniqueIds; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/NSArray+OWS.m b/SignalUtilitiesKit/Utilities/NSArray+OWS.m deleted file mode 100644 index cb6c06376..000000000 --- a/SignalUtilitiesKit/Utilities/NSArray+OWS.m +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import "NSArray+OWS.h" -#import "TSYapDatabaseObject.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation NSArray (OWS) - -- (NSArray *)uniqueIds -{ - NSMutableArray *result = [NSMutableArray new]; - for (id object in self) { - OWSAssertDebug([object isKindOfClass:[TSYapDatabaseObject class]]); - TSYapDatabaseObject *dbObject = object; - [result addObject:dbObject.uniqueId]; - } - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/NSObject+Casting.h b/SignalUtilitiesKit/Utilities/NSObject+Casting.h deleted file mode 100644 index 4b502bd31..000000000 --- a/SignalUtilitiesKit/Utilities/NSObject+Casting.h +++ /dev/null @@ -1,7 +0,0 @@ -#import - -@interface NSObject (Casting) - -- (id)as:(Class)cls; - -@end diff --git a/SignalUtilitiesKit/Utilities/NSObject+Casting.m b/SignalUtilitiesKit/Utilities/NSObject+Casting.m deleted file mode 100644 index 33afb994e..000000000 --- a/SignalUtilitiesKit/Utilities/NSObject+Casting.m +++ /dev/null @@ -1,10 +0,0 @@ -#import "NSObject+Casting.h" - -@implementation NSObject (Casting) - -- (id)as:(Class)cls { - if ([self isKindOfClass:cls]) { return self; } - return nil; -} - -@end diff --git a/SignalUtilitiesKit/Utilities/NSSet+Functional.h b/SignalUtilitiesKit/Utilities/NSSet+Functional.h deleted file mode 100644 index 14932e2ff..000000000 --- a/SignalUtilitiesKit/Utilities/NSSet+Functional.h +++ /dev/null @@ -1,9 +0,0 @@ -#import - -@interface NSSet (Functional) - -- (BOOL)contains:(BOOL (^)(id))predicate; -- (NSSet *)filtered:(BOOL (^)(id))isIncluded; -- (NSSet *)map:(id (^)(id))transform; - -@end diff --git a/SignalUtilitiesKit/Utilities/NSSet+Functional.m b/SignalUtilitiesKit/Utilities/NSSet+Functional.m deleted file mode 100644 index c19d814fd..000000000 --- a/SignalUtilitiesKit/Utilities/NSSet+Functional.m +++ /dev/null @@ -1,32 +0,0 @@ -#import "NSSet+Functional.h" - -@implementation NSSet (Functional) - -- (BOOL)contains:(BOOL (^)(id))predicate { - for (id object in self) { - BOOL isPredicateSatisfied = predicate(object); - if (isPredicateSatisfied) { return YES; } - } - return NO; -} - -- (NSSet *)filtered:(BOOL (^)(id))isIncluded { - NSMutableSet *result = [NSMutableSet new]; - for (id object in self) { - if (isIncluded(object)) { - [result addObject:object]; - } - } - return result; -} - -- (NSSet *)map:(id (^)(id))transform { - NSMutableSet *result = [NSMutableSet new]; - for (id object in self) { - id transformedObject = transform(object); - [result addObject:transformedObject]; - } - return result; -} - -@end diff --git a/SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.h b/SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.h deleted file mode 100644 index ce356c118..000000000 --- a/SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -NSString *NSStringForUIGestureRecognizerState(UIGestureRecognizerState state); - -// This custom GR can be used to detect touches when they -// begin in a view. In order to honor touch dispatch, this -// GR will ignore touches that: -// -// * Are not single touches. -// * Are not in the view for this GR. -// * Are inside a visible, interaction-enabled subview. -@interface OWSAnyTouchGestureRecognizer : UIGestureRecognizer - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.m b/SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.m deleted file mode 100644 index bd3f849cb..000000000 --- a/SignalUtilitiesKit/Utilities/OWSAnyTouchGestureRecognizer.m +++ /dev/null @@ -1,132 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSAnyTouchGestureRecognizer.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -NSString *NSStringForUIGestureRecognizerState(UIGestureRecognizerState state) -{ - switch (state) { - case UIGestureRecognizerStatePossible: - return @"UIGestureRecognizerStatePossible"; - case UIGestureRecognizerStateBegan: - return @"UIGestureRecognizerStateBegan"; - case UIGestureRecognizerStateChanged: - return @"UIGestureRecognizerStateChanged"; - case UIGestureRecognizerStateEnded: - return @"UIGestureRecognizerStateEnded"; - case UIGestureRecognizerStateCancelled: - return @"UIGestureRecognizerStateCancelled"; - case UIGestureRecognizerStateFailed: - return @"UIGestureRecognizerStateFailed"; - } -} - -@implementation OWSAnyTouchGestureRecognizer - -- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer -{ - return NO; -} - -- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer -{ - return NO; -} - -- (BOOL)shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer -{ - return NO; -} - -- (BOOL)shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer -{ - return YES; -} - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesBegan:touches withEvent:event]; - - if (self.state == UIGestureRecognizerStatePossible && [self isValidTouch:touches event:event]) { - self.state = UIGestureRecognizerStateRecognized; - } else { - self.state = UIGestureRecognizerStateFailed; - } -} - -- (UIView *)rootViewInViewHierarchy:(UIView *)view -{ - OWSAssertDebug(view); - UIResponder *responder = view; - UIView *lastView = nil; - while (responder) { - if ([responder isKindOfClass:[UIView class]]) { - lastView = (UIView *)responder; - } - responder = [responder nextResponder]; - } - return lastView; -} - -- (BOOL)isValidTouch:(NSSet *)touches event:(UIEvent *)event -{ - if (event.allTouches.count > 1) { - return NO; - } - if (touches.count != 1) { - return NO; - } - - UITouch *touch = touches.anyObject; - CGPoint location = [touch locationInView:self.view]; - if (!CGRectContainsPoint(self.view.bounds, location)) { - return NO; - } - - if ([self subviewControlOfView:self.view containsTouch:touch]) { - return NO; - } - - // Ignore touches that start near the top or bottom edge of the screen; - // they may be a system edge swipe gesture. - UIView *rootView = [self rootViewInViewHierarchy:self.view]; - CGPoint rootLocation = [touch locationInView:rootView]; - CGFloat distanceToTopEdge = MAX(0, rootLocation.y); - CGFloat distanceToBottomEdge = MAX(0, rootView.bounds.size.height - rootLocation.y); - CGFloat distanceToNearestEdge = MIN(distanceToTopEdge, distanceToBottomEdge); - CGFloat kSystemEdgeSwipeTolerance = 50.f; - if (distanceToNearestEdge < kSystemEdgeSwipeTolerance) { - return NO; - } - - return YES; -} - -- (BOOL)subviewControlOfView:(UIView *)superview containsTouch:(UITouch *)touch -{ - for (UIView *subview in superview.subviews) { - if (subview.hidden || !subview.userInteractionEnabled) { - continue; - } - CGPoint location = [touch locationInView:subview]; - if (!CGRectContainsPoint(subview.bounds, location)) { - continue; - } - if ([subview isKindOfClass:[UIControl class]]) { - return YES; - } - if ([self subviewControlOfView:subview containsTouch:touch]) { - return YES; - } - } - - return NO; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/UIGestureRecognizer+OWS.swift b/SignalUtilitiesKit/Utilities/UIGestureRecognizer+OWS.swift index 01dad6400..5baf4ac43 100644 --- a/SignalUtilitiesKit/Utilities/UIGestureRecognizer+OWS.swift +++ b/SignalUtilitiesKit/Utilities/UIGestureRecognizer+OWS.swift @@ -3,10 +3,25 @@ // import Foundation +import UIKit extension UIGestureRecognizer { @objc public var stateString: String { - return NSStringForUIGestureRecognizerState(state) + return state.asString + } +} + +extension UIGestureRecognizer.State { + fileprivate var asString: String { + switch self { + case .possible: return "UIGestureRecognizerStatePossible" + case .began: return "UIGestureRecognizerStateBegan" + case .changed: return "UIGestureRecognizerStateChanged" + case .ended: return "UIGestureRecognizerStateEnded" + case .cancelled: return "UIGestureRecognizerStateCancelled" + case .failed: return "UIGestureRecognizerStateFailed" + @unknown default: return "UIGestureRecognizerStateUnknown" + } } }