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" + } } }