diff --git a/LibSession-Util b/LibSession-Util index d8f07fa92..e3ccf29db 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit d8f07fa92c12c5c2409774e03e03395d7847d1c2 +Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 7891d2d2e..00a906857 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ 3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3496955F21A2FC8100DCFE74 /* CloudKit.framework */; }; 34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; }; 34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaAlbumView.swift */; }; - 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B0796B1FCF46B000E248C2 /* MainAppContext.m */; }; 34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */; }; 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */; }; 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */; }; @@ -313,27 +312,13 @@ C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6F255A57FA00E217F9 /* ReachabilityManager.swift */; }; C33FDC45255A581F00E217F9 /* AppVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8B255A57FD00E217F9 /* AppVersion.m */; }; C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; }; - C33FDC78255A582000E217F9 /* TSConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDABE255A580100E217F9 /* TSConstants.m */; }; C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADE255A580400E217F9 /* SwiftSingletons.swift */; }; - C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE0255A580400E217F9 /* ByteParser.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 */; }; C33FDD06255A582000E217F9 /* AppVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB4C255A580D00E217F9 /* AppVersion.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB69255A580F00E217F9 /* FeatureFlags.swift */; }; - C33FDD32255A582000E217F9 /* OWSOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB78255A581000E217F9 /* OWSOperation.m */; }; C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB80255A581100E217F9 /* Notification+Loki.swift */; }; C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8F255A581200E217F9 /* ParamParser.swift */; }; - C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA1255A581400E217F9 /* OWSOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */; }; 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, ); }; }; - C33FDDB3255A582000E217F9 /* OWSError.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF9255A581C00E217F9 /* OWSError.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, ); }; }; - C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC16255A581E00E217F9 /* FunctionalUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */; }; C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471F4B25553AB000297E91 /* MessageReceiver+Decryption.swift */; }; @@ -527,6 +512,9 @@ FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; + FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */; }; + FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; }; + FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */; }; FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* NetworkType.swift */; }; FD23CE1F2A65269C0000B97C /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1E2A65269C0000B97C /* Crypto.swift */; }; FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */; }; @@ -657,6 +645,7 @@ FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */; }; FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; }; FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; + FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; }; FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */; }; @@ -766,6 +755,15 @@ FDBB25E12983909300F1508E /* ConfigConvoInfoVolatileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */; }; FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; }; FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; + FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; + FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; + FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; + FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */; }; + FDC13D522A16F22E007267C7 /* PushNotificationAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */; }; + FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */; }; + FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; + FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; + FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */; }; FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */; }; @@ -783,7 +781,7 @@ FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; - FDC4382F27B383AF00C60D73 /* PushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */; }; + FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */; }; FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; }; FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; }; FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; }; @@ -806,11 +804,13 @@ FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; }; FDC6D6F32860607300B04575 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7542807C4BB004C14C5 /* Environment.swift */; }; FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; }; + FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; + FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; }; FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */; }; FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */; }; @@ -820,6 +820,7 @@ FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; + FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE125222A837E4E002DA685 /* MainAppContext.swift */; }; FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */; }; FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; }; FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; @@ -904,6 +905,9 @@ FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */; }; FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */; }; FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */; }; + FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */; }; + FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; }; + FDFBB7542A2023EB00CA7350 /* BencodeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */; }; FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; }; FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Utilities.swift */; }; FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; @@ -1109,8 +1113,6 @@ 3496955F21A2FC8100DCFE74 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = ""; }; 34A8B3502190A40E00218A25 /* MediaAlbumView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaAlbumView.swift; sourceTree = ""; }; - 34B0796B1FCF46B000E248C2 /* MainAppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainAppContext.m; sourceTree = ""; }; - 34B0796C1FCF46B000E248C2 /* MainAppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainAppContext.h; sourceTree = ""; }; 34B0796E1FD07B1E00E248C2 /* SignalShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SignalShareExtension.entitlements; sourceTree = ""; }; 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerViewController.swift; sourceTree = ""; }; @@ -1412,9 +1414,7 @@ C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFileSystem.m; sourceTree = ""; }; C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseDispatchQueue.swift; sourceTree = ""; }; C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; - C33FDABE255A580100E217F9 /* TSConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSConstants.m; 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 = ""; }; C33FDAEF255A580500E217F9 /* NSData+Image.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Image.m"; sourceTree = ""; }; C33FDAF1255A580500E217F9 /* ThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailService.swift; sourceTree = ""; }; C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxiedContentDownloader.swift; sourceTree = ""; }; @@ -1422,7 +1422,6 @@ C33FDAFD255A580600E217F9 /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = ""; }; C33FDB01255A580700E217F9 /* AppReadiness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppReadiness.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 = ""; }; C33FDB1C255A580900E217F9 /* UIImage+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+OWS.h"; sourceTree = ""; }; C33FDB22255A580900E217F9 /* OWSMediaUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSMediaUtils.swift; sourceTree = ""; }; C33FDB29255A580A00E217F9 /* NSData+Image.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Image.h"; sourceTree = ""; }; @@ -1430,7 +1429,6 @@ C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackgroundTask.h; sourceTree = ""; }; C33FDB3A255A580B00E217F9 /* Poller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Poller.swift; sourceTree = ""; }; C33FDB3F255A580C00E217F9 /* String+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SSK.swift"; sourceTree = ""; }; - C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalIOSProto.swift; sourceTree = ""; }; C33FDB41255A580C00E217F9 /* MIMETypeUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIMETypeUtil.m; sourceTree = ""; }; C33FDB49255A580C00E217F9 /* WeakTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakTimer.swift; sourceTree = ""; }; C33FDB4C255A580D00E217F9 /* AppVersion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppVersion.h; sourceTree = ""; }; @@ -1441,27 +1439,17 @@ C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SNUserDefaults.swift; sourceTree = ""; }; C33FDB75255A581000E217F9 /* AppReadiness.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppReadiness.m; sourceTree = ""; }; C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSUserDefaults+OWS.m"; sourceTree = ""; }; - C33FDB78255A581000E217F9 /* OWSOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOperation.m; sourceTree = ""; }; C33FDB80255A581100E217F9 /* Notification+Loki.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+Loki.swift"; sourceTree = ""; }; C33FDB81255A581100E217F9 /* UIImage+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+OWS.m"; sourceTree = ""; }; C33FDB85255A581100E217F9 /* AppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppContext.m; sourceTree = ""; }; C33FDB8A255A581200E217F9 /* AppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppContext.h; sourceTree = ""; }; C33FDB8F255A581200E217F9 /* ParamParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParamParser.swift; sourceTree = ""; }; - C33FDBA1255A581400E217F9 /* OWSOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOperation.h; sourceTree = ""; }; C33FDBA8255A581500E217F9 /* LinkPreviewDraft.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkPreviewDraft.swift; sourceTree = ""; }; C33FDBAB255A581500E217F9 /* OWSFileSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFileSystem.h; sourceTree = ""; }; - C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLSessionDataTask+StatusCode.m"; sourceTree = ""; }; C33FDBB6255A581600E217F9 /* DataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataSource.m; sourceTree = ""; }; C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKKeychainStorage.swift; sourceTree = ""; }; C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSignalAddress.swift; sourceTree = ""; }; - C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalIOS.pb.swift; sourceTree = ""; }; C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationAPI.swift; sourceTree = ""; }; - C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLSessionDataTask+StatusCode.h"; sourceTree = ""; }; - C33FDBF9255A581C00E217F9 /* OWSError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSError.h; 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 = ""; }; - C33FDC12255A581E00E217F9 /* TSConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSConstants.h; sourceTree = ""; }; - C33FDC16255A581E00E217F9 /* FunctionalUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FunctionalUtil.h; sourceTree = ""; }; C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackgroundTask.m; sourceTree = ""; }; C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+Encryption.swift"; sourceTree = ""; }; C3471F4B25553AB000297E91 /* MessageReceiver+Decryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Decryption.swift"; sourceTree = ""; }; @@ -1680,6 +1668,9 @@ FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistableRecordUtilitiesSpec.swift; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; + FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Utilities.swift"; sourceTree = ""; }; + FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = ""; }; + FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationRequirement.swift; sourceTree = ""; }; FD23CE1A2A651E6D0000B97C /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = ""; }; FD23CE1E2A65269C0000B97C /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+Crypto.swift"; sourceTree = ""; }; @@ -1772,6 +1763,7 @@ FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = ""; }; FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = ""; }; FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = ""; }; @@ -1871,6 +1863,15 @@ FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigConvoInfoVolatileSpec.swift; sourceTree = ""; }; FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = ""; }; FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; + FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = ""; }; + FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; + FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = ""; }; + FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIEndpoint.swift; sourceTree = ""; }; + FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIRequest.swift; sourceTree = ""; }; + FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupRequest.swift; sourceTree = ""; }; + FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = ""; }; + FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = ""; }; + FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotifyRequest.swift; sourceTree = ""; }; FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequestSpec.swift; sourceTree = ""; }; @@ -1887,7 +1888,7 @@ FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIError.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = ""; }; - FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushServerResponse.swift; sourceTree = ""; }; + FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPushServerResponse.swift; sourceTree = ""; }; FDC4383727B3863200C60D73 /* VersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionResponse.swift; sourceTree = ""; }; FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; @@ -1911,11 +1912,13 @@ FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = ""; }; FDC438CC27BC641200C60D73 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; + FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = ""; }; FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Differentiable+Utilities.swift"; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = ""; }; FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; + FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = ""; }; FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed2-2023-2y.crt"; sourceTree = ""; }; FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed1-2023-2y.crt"; sourceTree = ""; }; @@ -1925,6 +1928,7 @@ FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed3-2023-2y.der"; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = ""; }; + FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = ""; }; FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CryptoKit+Utilities.swift"; sourceTree = ""; }; FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = ""; }; FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; @@ -2012,6 +2016,9 @@ FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = ""; }; FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionCell+Styling.swift"; sourceTree = ""; }; FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentUserPoller.swift; sourceTree = ""; }; + FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = ""; }; + FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMetadata.swift; sourceTree = ""; }; + FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeSpec.swift; sourceTree = ""; }; FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGeneralCache.swift; sourceTree = ""; }; FDFDE123282D04F20098B17F /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = ""; }; @@ -3113,6 +3120,7 @@ C379DC6825672B5E0002D4EB /* Notifications */ = { isa = PBXGroup; children = ( + FDC13D4E2A16EE41007267C7 /* Types */, FDC4382D27B383A600C60D73 /* Models */, FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */, C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */, @@ -3355,34 +3363,20 @@ FD71161D28D9772700B47552 /* UIViewController+OWS.swift */, C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */, C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */, - C33FDBF9255A581C00E217F9 /* OWSError.h */, - C33FDC0B255A581D00E217F9 /* OWSError.m */, - C33FDBA1255A581400E217F9 /* OWSOperation.h */, - C33FDB78255A581000E217F9 /* OWSOperation.m */, C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */, C33FDA6F255A57FA00E217F9 /* ReachabilityManager.swift */, - C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */, - C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */, - C33FDC12255A581E00E217F9 /* TSConstants.h */, - C33FDABE255A580100E217F9 /* TSConstants.m */, C33FDB4C255A580D00E217F9 /* AppVersion.h */, + C33FDA8B255A57FD00E217F9 /* AppVersion.m */, C38EF3E4255B6DF4007E1867 /* CommonStrings.swift */, C38EF304255B6DBE007E1867 /* ImageCache.swift */, C38EF2F2255B6DBC007E1867 /* Searcher.swift */, C3F0A52F255C80BC007BE2A3 /* NoopNotificationsManager.swift */, - C33FDA8B255A57FD00E217F9 /* AppVersion.m */, C33FDB69255A580F00E217F9 /* FeatureFlags.swift */, C33FDB80255A581100E217F9 /* Notification+Loki.swift */, - C33FDC16255A581E00E217F9 /* FunctionalUtil.h */, - C33FDB17255A580800E217F9 /* FunctionalUtil.m */, C33FDB8F255A581200E217F9 /* ParamParser.swift */, C33FDADE255A580400E217F9 /* SwiftSingletons.swift */, C33FDB49255A580C00E217F9 /* WeakTimer.swift */, C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */, - C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */, - C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */, - C33FDC03255A581D00E217F9 /* ByteParser.h */, - C33FDAE0255A580400E217F9 /* ByteParser.m */, C38EF3DD255B6DF1007E1867 /* UIAlertController+OWS.swift */, C38EF241255B6D67007E1867 /* Collection+OWS.swift */, C38EF3AE255B6DE5007E1867 /* OrderedDictionary.swift */, @@ -3412,8 +3406,7 @@ 34330A581E7875FB00DF2FB9 /* Fonts */, B66DBF4919D5BBC8006EA940 /* Images.xcassets */, 45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */, - 34B0796C1FCF46B000E248C2 /* MainAppContext.h */, - 34B0796B1FCF46B000E248C2 /* MainAppContext.m */, + FDE125222A837E4E002DA685 /* MainAppContext.swift */, C3CA3AA0255CDA7000F4C6D4 /* Mnemonic */, B67EBF5C19194AC60084CCFD /* Settings.bundle */, B657DDC91911A40500F45B0C /* Signal.entitlements */, @@ -3540,6 +3533,7 @@ FD09796527F6B0A800936362 /* Utilities */ = { isa = PBXGroup; children = ( + FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */, @@ -3605,6 +3599,7 @@ FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */, FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, + FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */, ); path = Migrations; sourceTree = ""; @@ -3653,6 +3648,7 @@ children = ( FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */, FD17D7B727F51ECA00122BE0 /* Migration.swift */, + FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */, FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */, FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */, @@ -3725,6 +3721,7 @@ FD2B4B022949886900AB4848 /* Database */ = { isa = PBXGroup; children = ( + FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */, FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */, ); path = Database; @@ -3980,6 +3977,7 @@ FD83B9B927CF20A5005E1583 /* General */, FDDF074829DAB35200E5E8B5 /* JobRunner */, FD9B30F1293EA0AF008DEE3E /* Networking */, + FDFBB7522A2023DE00CA7350 /* Utilities */, FD29598E2A43BE5400888A17 /* Utilities */, ); path = SessionUtilitiesKitTests; @@ -4118,6 +4116,16 @@ path = Configs; sourceTree = ""; }; + FDC13D4E2A16EE41007267C7 /* Types */ = { + isa = PBXGroup; + children = ( + FDC13D482A16EC20007267C7 /* Service.swift */, + FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */, + FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */, + ); + path = Types; + sourceTree = ""; + }; FDC2909227D710A9005DAE71 /* Types */ = { isa = PBXGroup; children = ( @@ -4168,7 +4176,17 @@ FDC4382D27B383A600C60D73 /* Models */ = { isa = PBXGroup; children = ( - FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */, + FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */, + FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */, + FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */, + FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */, + FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */, + FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */, + FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */, + FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */, + FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */, + FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */, + FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */, ); path = Models; sourceTree = ""; @@ -4368,6 +4386,14 @@ path = Models; sourceTree = ""; }; + FDFBB7522A2023DE00CA7350 /* Utilities */ = { + isa = PBXGroup; + children = ( + FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */, + ); + path = Utilities; + sourceTree = ""; + }; FDFDE122282D04E30098B17F /* Transitions */ = { isa = PBXGroup; children = ( @@ -4394,12 +4420,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */, - C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */, - C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */, - C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */, - C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */, - C33FDDB3255A582000E217F9 /* OWSError.h in Headers */, C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */, C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */, C33FDD06255A582000E217F9 /* AppVersion.h in Headers */, @@ -4700,6 +4720,7 @@ D221A080169C9E5E00537ABF /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; DefaultBuildSystemTypeForWorkspace = Original; LastSwiftUpdateCheck = 1430; LastTestingUpgradeCheck = 0600; @@ -5461,10 +5482,8 @@ C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */, C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */, C38EF38A255B6DD2007E1867 /* AttachmentCaptionToolbar.swift in Sources */, - C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */, C38EF402255B6DF7007E1867 /* CommonStrings.swift in Sources */, C38EF3C1255B6DE7007E1867 /* ImageEditorBrushViewController.swift in Sources */, - C33FDD32255A582000E217F9 /* OWSOperation.m in Sources */, C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */, C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */, C38EF388255B6DD2007E1867 /* AttachmentApprovalViewController.swift in Sources */, @@ -5472,7 +5491,6 @@ C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */, C38EF407255B6DF7007E1867 /* Toast.swift in Sources */, C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */, - C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */, C33FDC45255A581F00E217F9 /* AppVersion.m in Sources */, C38EF3C7255B6DE7007E1867 /* ImageEditorCanvasView.swift in Sources */, C38EF400255B6DF7007E1867 /* GalleryRailView.swift in Sources */, @@ -5481,7 +5499,6 @@ C38EF3BA255B6DE7007E1867 /* ImageEditorItem.swift in Sources */, C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */, - C33FDCFA255A582000E217F9 /* SignalIOSProto.swift in Sources */, C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */, C38EF38B255B6DD2007E1867 /* AttachmentPrepViewController.swift in Sources */, C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */, @@ -5497,9 +5514,7 @@ FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */, C38EF401255B6DF7007E1867 /* VideoPlayerView.swift in Sources */, C38EF3BD255B6DE7007E1867 /* ImageEditorTransform.swift in Sources */, - C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */, C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */, - C33FDC78255A582000E217F9 /* TSConstants.m in Sources */, C38EF324255B6DBF007E1867 /* Bench.swift in Sources */, FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */, C38EF3F9255B6DF7007E1867 /* OWSLayerView.swift in Sources */, @@ -5511,12 +5526,10 @@ C38EF386255B6DD2007E1867 /* AttachmentApprovalInputAccessoryView.swift in Sources */, B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */, C38EF331255B6DBF007E1867 /* UIGestureRecognizer+OWS.swift in Sources */, - C33FDDC5255A582000E217F9 /* OWSError.m in Sources */, FD848B9C284435D7000E298B /* AppSetup.swift in Sources */, C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */, C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */, C38EF3BE255B6DE7007E1867 /* OrderedDictionary.swift in Sources */, - C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5617,6 +5630,7 @@ C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */, C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, + FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, @@ -5632,6 +5646,7 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */, + FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */, FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */, FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, @@ -5716,6 +5731,7 @@ FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, 7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */, + FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */, FD09799927FFC1A300936362 /* Attachment.swift in Sources */, FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */, C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */, @@ -5735,6 +5751,7 @@ FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */, FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */, FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */, + FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */, FD716E6A2850327900C96BF4 /* EndCallMode.swift in Sources */, FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */, 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */, @@ -5748,6 +5765,7 @@ FD245C57285065F100B966DD /* Poller.swift in Sources */, FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */, FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */, + FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */, @@ -5762,6 +5780,7 @@ FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, FD2B4AFD294688D000AB4848 /* SessionUtil+Contacts.swift in Sources */, 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */, + FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */, FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */, FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */, @@ -5770,18 +5789,22 @@ C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */, + FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */, B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */, FD8ECF8B2935DB4B00C0D1BB /* SharedConfigMessage.swift in Sources */, FD09798727FD1B7800936362 /* GroupMember.swift in Sources */, FDB4BBC92839BEF000B7C95D /* ProfileManagerError.swift in Sources */, + FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */, FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */, FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */, FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */, B8DE1FB426C22F2F0079C9CE /* WebRTCSession.swift in Sources */, + FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */, FDC6D6F32860607300B04575 /* Environment.swift in Sources */, C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */, FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */, + FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, FD8ECF892935AB7200C0D1BB /* SessionUtilError.swift in Sources */, @@ -5807,16 +5830,18 @@ FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */, FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */, FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */, - FDC4382F27B383AF00C60D73 /* PushServerResponse.swift in Sources */, + FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */, FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */, FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */, FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */, FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */, FD43EE9D297A5190009C87C5 /* SessionUtil+UserGroups.swift in Sources */, FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */, + FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */, FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, + FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */, FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */, FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */, FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */, @@ -5842,7 +5867,9 @@ FD09796E27FA6D0000936362 /* Contact.swift in Sources */, C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */, FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */, + FDC13D492A16EC20007267C7 /* Service.swift in Sources */, FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */, + FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */, @@ -5866,17 +5893,21 @@ FD245C682850666300B966DD /* Message+Destination.swift in Sources */, FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */, FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */, + FDC13D522A16F22E007267C7 /* PushNotificationAPIRequest.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */, C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */, C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */, FD2959922A4417A900888A17 /* PreparedSendData.swift in Sources */, FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */, + FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */, FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */, FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */, FD09797027FA6FF300936362 /* Profile.swift in Sources */, FD245C56285065EA00B966DD /* SNProto.swift in Sources */, FD09798B27FD1CFE00936362 /* Capability.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, + FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */, + FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */, FD09798127FCFEE800936362 /* SessionThread.swift in Sources */, FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, @@ -5939,10 +5970,10 @@ B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */, 3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */, 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */, + FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */, 7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, - 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */, 34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */, FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, @@ -6128,6 +6159,7 @@ FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, + FDFBB7542A2023EB00CA7350 /* BencodeSpec.swift in Sources */, FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, @@ -6370,7 +6402,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 421; + CURRENT_PROJECT_VERSION = 422; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6394,7 +6426,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6442,7 +6474,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 421; + CURRENT_PROJECT_VERSION = 422; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6471,7 +6503,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6507,7 +6539,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 421; + CURRENT_PROJECT_VERSION = 422; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6530,7 +6562,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6581,7 +6613,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 421; + CURRENT_PROJECT_VERSION = 422; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6609,7 +6641,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -7541,7 +7573,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 421; + CURRENT_PROJECT_VERSION = 422; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7579,7 +7611,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.4.0; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7612,7 +7644,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 421; + CURRENT_PROJECT_VERSION = 422; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7650,7 +7682,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.4.0; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 53b392657..e0b92096e 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -9,6 +9,8 @@ import WebRTC import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit +import SessionUtilitiesKit +import SessionSnodeKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { @objc static let isEnabled = true diff --git a/Session/Calls/Call Management/SessionCallManager+Action.swift b/Session/Calls/Call Management/SessionCallManager+Action.swift index 6ac7c49cd..639c00130 100644 --- a/Session/Calls/Call Management/SessionCallManager+Action.swift +++ b/Session/Calls/Call Management/SessionCallManager+Action.swift @@ -2,6 +2,7 @@ import UIKit import GRDB +import SessionUtilitiesKit extension SessionCallManager { @discardableResult diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index c89cd5e23..81b3879a8 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -6,6 +6,7 @@ import GRDB import SessionMessagingKit import SignalCoreKit import SignalUtilitiesKit +import SessionUtilitiesKit public final class SessionCallManager: NSObject, CallManagerProtocol { let provider: CXProvider? diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 7646903a2..221531644 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -4,6 +4,7 @@ import UIKit import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit +import SessionUtilitiesKit final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { private static let swipeToOperateThreshold: CGFloat = 60 diff --git a/Session/Calls/Views & Modals/MiniCallView.swift b/Session/Calls/Views & Modals/MiniCallView.swift index 8d060eed4..7c74a18df 100644 --- a/Session/Calls/Views & Modals/MiniCallView.swift +++ b/Session/Calls/Views & Modals/MiniCallView.swift @@ -3,6 +3,7 @@ import UIKit import WebRTC import SessionUIKit +import SessionUtilitiesKit final class MiniCallView: UIView, RTCVideoViewDelegate { var callVC: CallVC diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 07475227c..2c408877e 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -7,6 +7,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit +import SessionUtilitiesKit final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate { private struct GroupMemberDisplayInfo: FetchableRecord, Equatable, Hashable, Decodable, Differentiable { diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 3fb3d0fb8..327a76c49 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -2,6 +2,7 @@ import UIKit import SessionMessagingKit +import SessionUtilitiesKit extension ContextMenuVC { struct Action { diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations/ConversationSearch.swift index 785578104..22c6539b4 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations/ConversationSearch.swift @@ -5,6 +5,7 @@ import GRDB import SignalUtilitiesKit import SignalCoreKit import SessionUIKit +import SessionUtilitiesKit public class StyledSearchController: UISearchController { public override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index bb568d5dc..2b0def3f6 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -11,6 +11,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit +import SessionSnodeKit extension ConversationVC: InputViewDelegate, diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index fa5656fa1..523cdd884 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -208,17 +208,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers }() private lazy var emptyStateLabel: UILabel = { - let text: String = String( - format: { - switch (viewModel.threadData.threadIsNoteToSelf, viewModel.threadData.canWrite) { - case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() - case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() - default: return "CONVERSATION_EMPTY_STATE".localized() - } - }(), - viewModel.threadData.displayName - ) - + let text: String = emptyStateText(for: viewModel.threadData) let result: UILabel = UILabel() result.accessibilityLabel = "Empty state label" result.translatesAutoresizingMaskIntoConstraints = false @@ -698,6 +688,24 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers self.viewModel.onInteractionChange = nil } + private func emptyStateText(for threadData: SessionThreadViewModel) -> String { + return String( + format: { + switch (threadData.threadIsNoteToSelf, threadData.canWrite) { + case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() + case (_, false): + return (threadData.profile?.blocksCommunityMessageRequests == true ? + "COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE".localized() : + "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() + ) + + default: return "CONVERSATION_EMPTY_STATE".localized() + } + }(), + threadData.displayName + ) + } + private func handleThreadUpdates(_ updatedThreadData: SessionThreadViewModel, initialLoad: Bool = false) { // Ensure the first load or a load when returning from a child screen runs without animations (if // we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition) @@ -738,17 +746,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers ) // Update the empty state - let text: String = String( - format: { - switch (updatedThreadData.threadIsNoteToSelf, updatedThreadData.canWrite) { - case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() - case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() - default: return "CONVERSATION_EMPTY_STATE".localized() - } - }(), - updatedThreadData.displayName - ) - + let text: String = emptyStateText(for: updatedThreadData) emptyStateLabel.attributedText = NSAttributedString(string: text) .adding( attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)], @@ -791,8 +789,10 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers updatedThreadData.threadRequiresApproval == true ) self?.messageRequestStackView.isHidden = ( - updatedThreadData.threadIsMessageRequest == false && - updatedThreadData.threadRequiresApproval == false + !updatedThreadData.canWrite || ( + updatedThreadData.threadIsMessageRequest == false && + updatedThreadData.threadRequiresApproval == false + ) ) self?.messageRequestBackgroundView.isHidden = (self?.messageRequestStackView.isHidden == true) self?.messageRequestDescriptionLabelBottomConstraint?.constant = (updatedThreadData.threadRequiresApproval == true ? -4 : -20) diff --git a/Session/Conversations/Input View/InputViewButton.swift b/Session/Conversations/Input View/InputViewButton.swift index 8bf158199..246510fed 100644 --- a/Session/Conversations/Input View/InputViewButton.swift +++ b/Session/Conversations/Input View/InputViewButton.swift @@ -2,6 +2,7 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit final class InputViewButton: UIView { private let icon: UIImage? diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index ef88e2082..337862eb5 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit final class CallMessageCell: MessageCell { private static let iconSize: CGFloat = 16 diff --git a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift index 87ee2f937..eb410d306 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift @@ -3,6 +3,7 @@ import UIKit import SessionMessagingKit import SignalCoreKit +import SessionUtilitiesKit public class MediaAlbumView: UIStackView { private let items: [Attachment] diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 0af198314..838eb94d2 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -6,6 +6,7 @@ import SessionUIKit import SessionMessagingKit import SignalCoreKit import SignalUtilitiesKit +import SessionUtilitiesKit public class MediaView: UIView { static let contentMode: UIView.ContentMode = .scaleAspectFill diff --git a/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift b/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift index 9af77393f..08ccc733c 100644 --- a/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift +++ b/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SignalCoreKit +import SessionUtilitiesKit @objc class TypingIndicatorView: UIStackView { // This represents the spacing between the dots diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 63ccdf71d..e32f7fa7f 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -2,6 +2,7 @@ import UIKit import SessionMessagingKit +import SessionUtilitiesKit public enum SwipeState { case began diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 40792b3fc..5246a9687 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -9,6 +9,7 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit +import SessionSnodeKit class ThreadSettingsViewModel: SessionTableViewModel { // MARK: - Config @@ -178,6 +179,7 @@ class ThreadSettingsViewModel: SessionTableViewModel = Atomic([:]) diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift index 6b91630da..9684e44b1 100644 --- a/Session/Home/Message Requests/MessageRequestsViewController.swift +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -6,6 +6,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit +import SessionUtilitiesKit class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController, UITableViewDelegate, UITableViewDataSource { private static let loadingHeaderHeight: CGFloat = 40 diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index 08e6eff32..f633e3b36 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import DifferenceKit import SignalUtilitiesKit +import SessionUtilitiesKit public class MessageRequestsViewModel { public typealias SectionModel = ArraySection diff --git a/Session/Home/New Conversation/NewDMVC.swift b/Session/Home/New Conversation/NewDMVC.swift index e4765fee1..d369dd933 100644 --- a/Session/Home/New Conversation/NewDMVC.swift +++ b/Session/Home/New Conversation/NewDMVC.swift @@ -7,6 +7,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit +import SessionSnodeKit final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate { private var shouldShowBackButton: Bool = true diff --git a/Session/Media Viewing & Editing/CropScaleImageViewController.swift b/Session/Media Viewing & Editing/CropScaleImageViewController.swift index df821b716..d307f9990 100644 --- a/Session/Media Viewing & Editing/CropScaleImageViewController.swift +++ b/Session/Media Viewing & Editing/CropScaleImageViewController.swift @@ -5,6 +5,7 @@ import MediaPlayer import SessionUIKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit // This kind of view is tricky. I've tried to organize things in the // simplest possible way. @@ -359,54 +360,54 @@ import SignalCoreKit @objc func handlePinch(sender: UIPinchGestureRecognizer) { switch sender.state { - case .possible: - break - case .began: - srcTranslationAtPinchStart = srcTranslation - imageScaleAtPinchStart = imageScale + case .possible: break + case .began: + srcTranslationAtPinchStart = srcTranslation + imageScaleAtPinchStart = imageScale - lastPinchLocation = - sender.location(in: sender.view) - lastPinchScale = sender.scale - break - case .changed, .ended: - if sender.numberOfTouches > 1 { - let location = + lastPinchLocation = sender.location(in: sender.view) - let scaleDiff = sender.scale / lastPinchScale - - // Update scaling. - let srcCropSizeBeforeScalePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, - height: srcDefaultCropSizePoints.height / imageScale) - imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale * scaleDiff)) - let srcCropSizeAfterScalePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, - height: srcDefaultCropSizePoints.height / imageScale) - // Since the translation state reflects the "upper left" corner of the crop region, we need to - // adjust the translation when scaling to preserve the "center" of the crop region. - srcTranslation.x += (srcCropSizeBeforeScalePoints.width - srcCropSizeAfterScalePoints.width) * 0.5 - srcTranslation.y += (srcCropSizeBeforeScalePoints.height - srcCropSizeAfterScalePoints.height) * 0.5 - - // Update translation. - let viewSizePoints = imageView.frame.size - let srcCropSizePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, - height: srcDefaultCropSizePoints.height / imageScale) - - let viewToSrcRatio = srcCropSizePoints.width / viewSizePoints.width - - let gestureTranslation = CGPoint(x: location.x - lastPinchLocation.x, - y: location.y - lastPinchLocation.y) - - srcTranslation = CGPoint(x: srcTranslation.x + gestureTranslation.x * -viewToSrcRatio, - y: srcTranslation.y + gestureTranslation.y * -viewToSrcRatio) - - lastPinchLocation = location lastPinchScale = sender.scale - } - break - case .cancelled, .failed: - srcTranslation = srcTranslationAtPinchStart - imageScale = imageScaleAtPinchStart - break + + case .changed, .ended: + if sender.numberOfTouches > 1 { + let location = + sender.location(in: sender.view) + let scaleDiff = sender.scale / lastPinchScale + + // Update scaling. + let srcCropSizeBeforeScalePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, + height: srcDefaultCropSizePoints.height / imageScale) + imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale * scaleDiff)) + let srcCropSizeAfterScalePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, + height: srcDefaultCropSizePoints.height / imageScale) + // Since the translation state reflects the "upper left" corner of the crop region, we need to + // adjust the translation when scaling to preserve the "center" of the crop region. + srcTranslation.x += (srcCropSizeBeforeScalePoints.width - srcCropSizeAfterScalePoints.width) * 0.5 + srcTranslation.y += (srcCropSizeBeforeScalePoints.height - srcCropSizeAfterScalePoints.height) * 0.5 + + // Update translation. + let viewSizePoints = imageView.frame.size + let srcCropSizePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, + height: srcDefaultCropSizePoints.height / imageScale) + + let viewToSrcRatio = srcCropSizePoints.width / viewSizePoints.width + + let gestureTranslation = CGPoint(x: location.x - lastPinchLocation.x, + y: location.y - lastPinchLocation.y) + + srcTranslation = CGPoint(x: srcTranslation.x + gestureTranslation.x * -viewToSrcRatio, + y: srcTranslation.y + gestureTranslation.y * -viewToSrcRatio) + + lastPinchLocation = location + lastPinchScale = sender.scale + } + + case .cancelled, .failed: + srcTranslation = srcTranslationAtPinchStart + imageScale = imageScaleAtPinchStart + + @unknown default: break } updateImageLayout() @@ -416,29 +417,28 @@ import SignalCoreKit @objc func handlePan(sender: UIPanGestureRecognizer) { switch sender.state { - case .possible: - break - case .began: - srcTranslationAtPanStart = srcTranslation - break - case .changed, .ended: - let viewSizePoints = imageView.frame.size - let srcCropSizePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, - height: srcDefaultCropSizePoints.height / imageScale) + case .possible: break + case .began: + srcTranslationAtPanStart = srcTranslation + + case .changed, .ended: + let viewSizePoints = imageView.frame.size + let srcCropSizePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, + height: srcDefaultCropSizePoints.height / imageScale) - let viewToSrcRatio = srcCropSizePoints.width / viewSizePoints.width + let viewToSrcRatio = srcCropSizePoints.width / viewSizePoints.width - let gestureTranslation = - sender.translation(in: sender.view) + let gestureTranslation = + sender.translation(in: sender.view) - // Update translation. - srcTranslation = CGPoint(x: srcTranslationAtPanStart.x + gestureTranslation.x * -viewToSrcRatio, - y: srcTranslationAtPanStart.y + gestureTranslation.y * -viewToSrcRatio) - break - case .cancelled, .failed: - srcTranslation - = srcTranslationAtPanStart - break + // Update translation. + srcTranslation = CGPoint(x: srcTranslationAtPanStart.x + gestureTranslation.x * -viewToSrcRatio, + y: srcTranslationAtPanStart.y + gestureTranslation.y * -viewToSrcRatio) + + case .cancelled, .failed: + srcTranslation = srcTranslationAtPanStart + + @unknown default: break } updateImageLayout() diff --git a/Session/Media Viewing & Editing/DocumentTitleViewController.swift b/Session/Media Viewing & Editing/DocumentTitleViewController.swift index b651a62cc..12ba3248c 100644 --- a/Session/Media Viewing & Editing/DocumentTitleViewController.swift +++ b/Session/Media Viewing & Editing/DocumentTitleViewController.swift @@ -7,6 +7,7 @@ import DifferenceKit import SessionUIKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift index 0967e8020..20575c640 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift @@ -5,6 +5,7 @@ import Combine import YYImage import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit class GifPickerCell: UICollectionViewCell { diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift index 4b2436258..d88e5bdb6 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift @@ -6,6 +6,7 @@ import Reachability import SignalUtilitiesKit import SessionUIKit import SignalCoreKit +import SessionUtilitiesKit class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollectionViewDataSource, UICollectionViewDelegate, GifPickerLayoutDelegate { diff --git a/Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift b/Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift index 3dd50b0c0..3afd8d56a 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift @@ -2,6 +2,7 @@ import Foundation import SignalUtilitiesKit +import SessionUtilitiesKit public class GiphyDownloader: ProxiedContentDownloader { diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index 37fb7f4a8..6983de804 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -6,6 +6,7 @@ import Photos import SessionUIKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit protocol ImagePickerGridControllerDelegate: AnyObject { func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) @@ -155,6 +156,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat case .cancelled, .ended, .failed: collectionView.isUserInteractionEnabled = true collectionView.isScrollEnabled = true + + @unknown default: break } } diff --git a/Session/Media Viewing & Editing/MediaDetailViewController.swift b/Session/Media Viewing & Editing/MediaDetailViewController.swift index 1a7abeebe..f3eaf40e6 100644 --- a/Session/Media Viewing & Editing/MediaDetailViewController.swift +++ b/Session/Media Viewing & Editing/MediaDetailViewController.swift @@ -6,6 +6,7 @@ import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit import SignalCoreKit +import SessionUtilitiesKit public enum MediaGalleryOption { case sliderEnabled diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index f0c725dae..1e14b2bdd 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -6,6 +6,8 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit +import SessionSnodeKit class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, MediaDetailViewControllerDelegate, InteractivelyDismissableViewController { class DynamicallySizedView: UIView { diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index 393e8411c..98d58dd5f 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -7,6 +7,7 @@ import DifferenceKit import SessionUIKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit public class MediaTileViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { diff --git a/Session/Media Viewing & Editing/PhotoCaptureViewController.swift b/Session/Media Viewing & Editing/PhotoCaptureViewController.swift index 91e43eb61..3dd46b425 100644 --- a/Session/Media Viewing & Editing/PhotoCaptureViewController.swift +++ b/Session/Media Viewing & Editing/PhotoCaptureViewController.swift @@ -6,6 +6,7 @@ import AVFoundation import SessionUIKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit protocol PhotoCaptureViewControllerDelegate: AnyObject { func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index 5bc29f7e5..0825a42cf 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -6,6 +6,7 @@ import Photos import CoreServices import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit protocol PhotoLibraryDelegate: AnyObject { func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift index 3ce3348fc..32cf44c16 100644 --- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift +++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift @@ -6,6 +6,7 @@ import Photos import SignalUtilitiesKit import SignalCoreKit import SessionUIKit +import SessionUtilitiesKit class SendMediaNavigationController: UINavigationController { public override var preferredStatusBarStyle: UIStatusBarStyle { @@ -539,8 +540,8 @@ private struct MediaLibrarySelection: Hashable, Equatable { let asset: PHAsset let signalAttachmentPublisher: AnyPublisher - var hashValue: Int { - return asset.hashValue + func hash(into hasher: inout Hasher) { + asset.hash(into: &hasher) } var publisher: AnyPublisher { @@ -559,8 +560,8 @@ private struct MediaLibraryAttachment: Hashable, Equatable { let asset: PHAsset let signalAttachment: SignalAttachment - public var hashValue: Int { - return asset.hashValue + func hash(into hasher: inout Hasher) { + asset.hash(into: &hasher) } public static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool { diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index d21a480c1..e04337994 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -9,6 +9,7 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit import SignalCoreKit +import SessionSnodeKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { diff --git a/Session/Meta/AppEnvironment.swift b/Session/Meta/AppEnvironment.swift index afcb1e868..35e906c9f 100644 --- a/Session/Meta/AppEnvironment.swift +++ b/Session/Meta/AppEnvironment.swift @@ -4,6 +4,7 @@ import Foundation import SessionUtilitiesKit import SignalUtilitiesKit import SignalCoreKit +import SessionMessagingKit public class AppEnvironment { diff --git a/Session/Meta/MainAppContext.h b/Session/Meta/MainAppContext.h deleted file mode 100644 index 6fab6a1ad..000000000 --- a/Session/Meta/MainAppContext.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -extern NSString *const ReportedApplicationStateDidChangeNotification; - -@interface MainAppContext : NSObject - -- (instancetype)init; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Session/Meta/MainAppContext.m b/Session/Meta/MainAppContext.m deleted file mode 100644 index 21daaa5f2..000000000 --- a/Session/Meta/MainAppContext.m +++ /dev/null @@ -1,314 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "MainAppContext.h" -#import "Session-Swift.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplicationStateDidChangeNotification"; - -@interface MainAppContext () - -@property (atomic) UIApplicationState reportedApplicationState; - -@property (nonatomic, nullable) NSMutableArray *appActiveBlocks; - -@end - -#pragma mark - - -@implementation MainAppContext - -@synthesize mainWindow = _mainWindow; -@synthesize appLaunchTime = _appLaunchTime; -@synthesize wasWokenUpByPushNotification = _wasWokenUpByPushNotification; - -- (instancetype)init -{ - self = [super init]; - - if (!self) { - return self; - } - - self.reportedApplicationState = UIApplicationStateInactive; - - _appLaunchTime = [NSDate new]; - _wasWokenUpByPushNotification = false; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillResignActive:) - name:UIApplicationWillResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidBecomeActive:) - name:UIApplicationDidBecomeActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillTerminate:) - name:UIApplicationWillTerminateNotification - object:nil]; - - self.appActiveBlocks = [NSMutableArray new]; - - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - Notifications - -- (void)setReportedApplicationState:(UIApplicationState)reportedApplicationState -{ - OWSAssertIsOnMainThread(); - - if (_reportedApplicationState == reportedApplicationState) { - return; - } - _reportedApplicationState = reportedApplicationState; - - [[NSNotificationCenter defaultCenter] postNotificationName:ReportedApplicationStateDidChangeNotification - object:nil - userInfo:nil]; -} - -- (void)applicationWillEnterForeground:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - self.reportedApplicationState = UIApplicationStateInactive; - - OWSLogInfo(@""); - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillEnterForegroundNotification object:nil]; -} - -- (void)applicationDidEnterBackground:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - self.reportedApplicationState = UIApplicationStateBackground; - - OWSLogInfo(@""); - [DDLog flushLog]; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidEnterBackgroundNotification object:nil]; -} - -- (void)applicationWillResignActive:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - self.reportedApplicationState = UIApplicationStateInactive; - - OWSLogInfo(@""); - [DDLog flushLog]; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillResignActiveNotification object:nil]; -} - -- (void)applicationDidBecomeActive:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - self.reportedApplicationState = UIApplicationStateActive; - - OWSLogInfo(@""); - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidBecomeActiveNotification object:nil]; - - [self runAppActiveBlocks]; -} - -- (void)applicationWillTerminate:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@""); - [DDLog flushLog]; -} - -#pragma mark - - -- (BOOL)isMainApp -{ - return YES; -} - -- (BOOL)isMainAppAndActive -{ - return [UIApplication sharedApplication].applicationState == UIApplicationStateActive; -} - -- (BOOL)isShareExtension { - return NO; -} - -- (BOOL)isRTL -{ - // FIXME: We should try to remove this as we've had to add a hack to ensure the first call to this runs on the main thread - static BOOL isRTL = NO; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - isRTL = [[UIApplication sharedApplication] userInterfaceLayoutDirection] - == UIUserInterfaceLayoutDirectionRightToLeft; - }); - return isRTL; -} - -- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated -{ - [[UIApplication sharedApplication] setStatusBarHidden:isHidden animated:isAnimated]; -} - -- (CGFloat)statusBarHeight -{ - return [UIApplication sharedApplication].statusBarFrame.size.height; -} - -- (BOOL)isInBackground -{ - return self.reportedApplicationState == UIApplicationStateBackground; -} - -- (BOOL)isAppForegroundAndActive -{ - return self.reportedApplicationState == UIApplicationStateActive; -} - -- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler: - (BackgroundTaskExpirationHandler)expirationHandler -{ - return [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:expirationHandler]; -} - -- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier -{ - [UIApplication.sharedApplication endBackgroundTask:backgroundTaskIdentifier]; -} - -- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray *)blockingObjects -{ - if (UIApplication.sharedApplication.isIdleTimerDisabled != shouldBeBlocking) { - if (shouldBeBlocking) { - NSMutableString *logString = - [NSMutableString stringWithFormat:@"Blocking sleep because of: %@", blockingObjects.firstObject]; - if (blockingObjects.count > 1) { - [logString appendString:[NSString stringWithFormat:@"(and %lu others)", blockingObjects.count - 1]]; - } - OWSLogInfo(@"%@", logString); - } else { - OWSLogInfo(@"Unblocking Sleep."); - } - } - UIApplication.sharedApplication.idleTimerDisabled = shouldBeBlocking; -} - -- (void)setMainAppBadgeNumber:(NSInteger)value -{ - [[UIApplication sharedApplication] setApplicationIconBadgeNumber:value]; - [[NSUserDefaults sharedLokiProject] setInteger:value forKey:@"currentBadgeNumber"]; - [[NSUserDefaults sharedLokiProject] synchronize]; -} - -- (nullable UIViewController *)frontmostViewController -{ - return UIApplication.sharedApplication.frontmostViewControllerIgnoringAlerts; -} - -- (nullable UIAlertAction *)openSystemSettingsAction -{ - return [UIAlertAction actionWithTitle:CommonStrings.openSettingsButton - accessibilityIdentifier:[NSString stringWithFormat:@"%@.%@", self.class, @"system_settings"] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { - [UIApplication.sharedApplication openSystemSettings]; - }]; -} - -- (void)setNetworkActivityIndicatorVisible:(BOOL)value -{ - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:value]; -} - -#pragma mark - - -- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block -{ - OWSAssertDebug(block); - - [Threading dispatchMainThreadSafe:^{ - if (self.isMainAppAndActive) { - // App active blocks typically will be used to safely access the - // shared data container, so use a background task to protect this - // work. - OWSBackgroundTask *_Nullable backgroundTask = - [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - block(); - OWSAssertDebug(backgroundTask); - backgroundTask = nil; - return; - } - - [self.appActiveBlocks addObject:block]; - }]; -} - -- (void)runAppActiveBlocks -{ - OWSAssertIsOnMainThread(); - OWSAssertDebug(self.isMainAppAndActive); - - // App active blocks typically will be used to safely access the - // shared data container, so use a background task to protect this - // work. - OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - - NSArray *appActiveBlocks = [self.appActiveBlocks copy]; - [self.appActiveBlocks removeAllObjects]; - for (AppActiveBlock block in appActiveBlocks) { - block(); - } - - OWSAssertDebug(backgroundTask); - backgroundTask = nil; -} - -- (NSString *)appDocumentDirectoryPath -{ - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *documentDirectoryURL = - [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - return [documentDirectoryURL path]; -} - -- (NSString *)appSharedDataDirectoryPath -{ - NSURL *groupContainerDirectoryURL = - [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup]; - return [groupContainerDirectoryURL path]; -} - -- (NSUserDefaults *)appUserDefaults -{ - return [[NSUserDefaults alloc] initWithSuiteName:SignalApplicationGroup]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Session/Meta/MainAppContext.swift b/Session/Meta/MainAppContext.swift new file mode 100644 index 000000000..949176192 --- /dev/null +++ b/Session/Meta/MainAppContext.swift @@ -0,0 +1,253 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SignalCoreKit +import SessionUtilitiesKit + +final class MainAppContext: NSObject, AppContext { + var reportedApplicationState: UIApplication.State + + let appLaunchTime = Date() + let isMainApp: Bool = true + var isMainAppAndActive: Bool { UIApplication.shared.applicationState == .active } + var isShareExtension: Bool = false + var appActiveBlocks: [AppActiveBlock] = [] + + var mainWindow: UIWindow? + var wasWokenUpByPushNotification: Bool = false + + private static var _isRTL: Bool = { + return (UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft) + }() + + var isRTL: Bool { return MainAppContext._isRTL } + + var statusBarHeight: CGFloat { UIApplication.shared.statusBarFrame.size.height } + var openSystemSettingsAction: UIAlertAction? { + let result = UIAlertAction( + title: "OPEN_SETTINGS_BUTTON".localized(), + style: .default + ) { _ in UIApplication.shared.openSystemSettings() } + result.accessibilityIdentifier = "\(type(of: self)).system_settings" + + return result + } + + // MARK: - Initialization + + override init() { + self.reportedApplicationState = .inactive + + super.init() + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillEnterForeground(notification:)), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground(notification:)), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillResignActive(notification:)), + name: UIApplication.willResignActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(notification:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillTerminate(notification:)), + name: UIApplication.willTerminateNotification, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Notifications + + @objc private func applicationWillEnterForeground(notification: NSNotification) { + AssertIsOnMainThread() + + self.reportedApplicationState = .inactive + OWSLogger.info("") + + NotificationCenter.default.post( + name: .OWSApplicationWillEnterForeground, + object: nil + ) + } + + @objc private func applicationDidEnterBackground(notification: NSNotification) { + AssertIsOnMainThread() + + self.reportedApplicationState = .background + + OWSLogger.info("") + DDLog.flushLog() + + NotificationCenter.default.post( + name: .OWSApplicationDidEnterBackground, + object: nil + ) + } + + @objc private func applicationWillResignActive(notification: NSNotification) { + AssertIsOnMainThread() + + self.reportedApplicationState = .inactive + + OWSLogger.info("") + DDLog.flushLog() + + NotificationCenter.default.post( + name: .OWSApplicationWillResignActive, + object: nil + ) + } + + @objc private func applicationDidBecomeActive(notification: NSNotification) { + AssertIsOnMainThread() + + self.reportedApplicationState = .active + + OWSLogger.info("") + + NotificationCenter.default.post( + name: .OWSApplicationDidBecomeActive, + object: nil + ) + + self.runAppActiveBlocks() + } + + @objc private func applicationWillTerminate(notification: NSNotification) { + AssertIsOnMainThread() + + OWSLogger.info("") + DDLog.flushLog() + } + + // MARK: - AppContext Functions + + func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { + UIApplication.shared.setStatusBarHidden(isHidden, with: (isAnimated ? .slide : .none)) + } + + func isAppForegroundAndActive() -> Bool { + return (reportedApplicationState == .active) + } + + func isInBackground() -> Bool { + return (reportedApplicationState == .background) + } + + func beginBackgroundTask(expirationHandler: @escaping BackgroundTaskExpirationHandler) -> UIBackgroundTaskIdentifier { + return UIApplication.shared.beginBackgroundTask(expirationHandler: expirationHandler) + } + + func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + + func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) { + if UIApplication.shared.isIdleTimerDisabled != shouldBeBlocking { + if shouldBeBlocking { + var logString: String = "Blocking sleep because of: \(String(describing: blockingObjects.first))" + + if blockingObjects.count > 1 { + logString = "\(logString) (and \(blockingObjects.count - 1) others)" + } + OWSLogger.info(logString) + } + else { + OWSLogger.info("Unblocking Sleep.") + } + } + UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking + } + + func setMainAppBadgeNumber(_ value: Int) { + UIApplication.shared.applicationIconBadgeNumber = value + UserDefaults.sharedLokiProject?.setValue(value, forKey: "currentBadgeNumber") + } + + func frontmostViewController() -> UIViewController? { + UIApplication.shared.frontmostViewControllerIgnoringAlerts + } + + func setNetworkActivityIndicatorVisible(_ value: Bool) { + UIApplication.shared.isNetworkActivityIndicatorVisible = value + } + + // MARK: - + + func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { + Threading.dispatchMainThreadSafe { [weak self] in + if self?.isMainAppAndActive == true { + // App active blocks typically will be used to safely access the + // shared data container, so use a background task to protect this + // work. + var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function) + block() + if backgroundTask != nil { backgroundTask = nil } + return + } + + self?.appActiveBlocks.append(block) + } + } + + func runAppActiveBlocks() { + // App active blocks typically will be used to safely access the + // shared data container, so use a background task to protect this + // work. + var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function) + + let appActiveBlocks: [AppActiveBlock] = self.appActiveBlocks + self.appActiveBlocks.removeAll() + + appActiveBlocks.forEach { $0() } + if backgroundTask != nil { backgroundTask = nil } + } + + func appDocumentDirectoryPath() -> String { + let targetPath: String? = FileManager.default + .urls( + for: .documentDirectory, + in: .userDomainMask + ) + .last? + .path + owsAssertDebug(targetPath != nil) + + return (targetPath ?? "") + } + + func appSharedDataDirectoryPath() -> String { + let targetPath: String? = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: UserDefaults.applicationGroup)? + .path + owsAssertDebug(targetPath != nil) + + return (targetPath ?? "") + } + + func appUserDefaults() -> UserDefaults { + owsAssertDebug(UserDefaults.sharedLokiProject != nil) + + return (UserDefaults.sharedLokiProject ?? UserDefaults.standard) + } +} diff --git a/Session/Meta/Signal-Bridging-Header.h b/Session/Meta/Signal-Bridging-Header.h index a745cd256..c9b6f4634 100644 --- a/Session/Meta/Signal-Bridging-Header.h +++ b/Session/Meta/Signal-Bridging-Header.h @@ -7,4 +7,3 @@ #import "OWSBezierPathView.h" #import "OWSMessageTimerView.h" #import "OWSWindowManager.h" -#import "MainAppContext.h" diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 300b4ddd9..123dbb603 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Bildschirmschutz"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Lesebestätigungen"; "PRIVACY_READ_RECEIPTS_TITLE" = "Lesebestätigungen"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 2201a7b94..f545916a2 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index b4e4a93bf..cae553c57 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Protección de pantalla"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Notificaciones de lectura"; "PRIVACY_READ_RECEIPTS_TITLE" = "Notificaciones de lectura"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index b0ca98c40..6f6790ebb 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "امنیت صفحه"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "قفل Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = " برای باز کردن قفل Session به شناسه لمسی، شناسه صورت و یا رمز عبوری ضرورت است."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "رسیدهای خواندن"; "PRIVACY_READ_RECEIPTS_TITLE" = "رسیدهای خواندن"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "رسیدهای خواندن در چت‌های یک به یک روان شود."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 81a232bb9..a6f58d1a2 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Näytön suojaus"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Lukukuittaukset"; "PRIVACY_READ_RECEIPTS_TITLE" = "Lukukuittaukset"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 6c0f23096..d7306e41a 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Sécurité de l’écran"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Verrouiller Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Requiert Touch ID, Face ID ou votre code pour déverrouiller Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Accusés de lecture"; "PRIVACY_READ_RECEIPTS_TITLE" = "Accusés de lecture"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Envoyer un accusé réception dans les conversations 1 à 1."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index a9770d46f..bec788e5b 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 21e842304..c2d19ccc4 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Sigurnost zaslona"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Potvrda o čitanju"; "PRIVACY_READ_RECEIPTS_TITLE" = "Potvrda o čitanju"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 65c0b898a..859fe512b 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Layar Aman"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Pesan terbaca diterima"; "PRIVACY_READ_RECEIPTS_TITLE" = "Pesan terbaca diterima"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index b7d7d7f00..ea8c7f513 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Sicurezza schermo"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Ricevute di lettura"; "PRIVACY_READ_RECEIPTS_TITLE" = "Ricevute di lettura"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index cec39bfdb..fc0d5deea 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "スクリーン・セキュリティ"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "既読確認"; "PRIVACY_READ_RECEIPTS_TITLE" = "既読確認"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 4a40da063..159e6b21c 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Scherm beveiliging"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Leesbevestigingen"; "PRIVACY_READ_RECEIPTS_TITLE" = "Leesbevestigingen"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 782cac2aa..e5a8e800c 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Ochrona ekranu"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Potwierdzenia odczytania"; "PRIVACY_READ_RECEIPTS_TITLE" = "Potwierdzenia odczytania"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 7d65ae5a5..d027c3b0b 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Segurança de Tela"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Confirmações de Leitura"; "PRIVACY_READ_RECEIPTS_TITLE" = "Confirmações de Leitura"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 840d8ea30..d34ddc95a 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Защита экрана"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Уведомления о прочтении"; "PRIVACY_READ_RECEIPTS_TITLE" = "Уведомления о прочтении"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 6fa01dfb7..06f1a6bde 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "තිරයේ ආරක්ෂාව"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "කියවූ බවට ලදුපත්"; "PRIVACY_READ_RECEIPTS_TITLE" = "කියවූ බවට ලදුපත්"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index e6bb1b17d..3505fdb76 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Zabezpečenie obrazovky"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Potvrdenia o prečítaní"; "PRIVACY_READ_RECEIPTS_TITLE" = "Potvrdenia o prečítaní"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 4b80414de..6716e1447 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Skärmsäkerhet"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Läskvittenser"; "PRIVACY_READ_RECEIPTS_TITLE" = "Läskvittenser"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index c2856e17c..1cfa64ef0 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "ความปลอดภัยหน้าจอ"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "แจ้งการอ่านข้อความ"; "PRIVACY_READ_RECEIPTS_TITLE" = "แจ้งการอ่านข้อความ"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 54665d063..bd1db2583 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 246631d27..48ee16204 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "螢幕顯示安全"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "已讀回條"; "PRIVACY_READ_RECEIPTS_TITLE" = "已讀回條"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index c21ee5fe4..b5e3881fd 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -484,6 +484,9 @@ "PRIVACY_SECTION_SCREEN_SECURITY" = "屏幕安全"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; +"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests"; +"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations."; "PRIVACY_SECTION_READ_RECEIPTS" = "已读回执"; "PRIVACY_READ_RECEIPTS_TITLE" = "已读回执"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; @@ -646,6 +649,8 @@ "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; +"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; +"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again."; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index bb8129f9a..2c0774346 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -6,6 +6,8 @@ import GRDB import SessionMessagingKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit +import SessionSnodeKit /// There are two primary components in our system notification integration: /// diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 04d4a4e0b..6645428a5 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -4,8 +4,10 @@ import Foundation import Combine import PushKit import GRDB +import SessionMessagingKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit public enum PushRegistrationError: Error { case assertionError(description: String) @@ -53,8 +55,6 @@ public enum PushRegistrationError: Error { Logger.info("") return registerUserNotificationSettings() - .subscribe(on: DispatchQueue.global(qos: .default)) - .receive(on: DispatchQueue.main) // MUST be on main thread .setFailureType(to: Error.self) .tryFlatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in #if targetEnvironment(simulator) @@ -75,24 +75,27 @@ public enum PushRegistrationError: Error { // MARK: Vanilla push token // Vanilla push token is obtained from the system via AppDelegate - public func didReceiveVanillaPushToken(_ tokenData: Data) { + public func didReceiveVanillaPushToken(_ tokenData: Data, using dependencies: Dependencies = Dependencies()) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { owsFailDebug("publisher completion in \(#function) unexpectedly nil") return } - vanillaTokenResolver(Result.success(tokenData)) + DispatchQueue.global(qos: .default).async(using: dependencies) { + vanillaTokenResolver(Result.success(tokenData)) + } } // Vanilla push token is obtained from the system via AppDelegate - @objc - public func didFailToReceiveVanillaPushToken(error: Error) { + public func didFailToReceiveVanillaPushToken(error: Error, using dependencies: Dependencies = Dependencies()) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { owsFailDebug("publisher completion in \(#function) unexpectedly nil") return } - vanillaTokenResolver(Result.failure(error)) + DispatchQueue.global(qos: .default).async(using: dependencies) { + vanillaTokenResolver(Result.failure(error)) + } } // MARK: helpers @@ -111,9 +114,8 @@ public enum PushRegistrationError: Error { * in this case we've verified that we *have* properly registered notification settings. */ private var isSusceptibleToFailedPushRegistration: Bool { - // Only affects users who have disabled both: background refresh *and* notifications - guard UIApplication.shared.backgroundRefreshStatus == .denied else { + guard DispatchQueue.main.sync(execute: { UIApplication.shared.backgroundRefreshStatus }) == .denied else { return false } @@ -128,10 +130,7 @@ public enum PushRegistrationError: Error { return true } - // FIXME: Might be nice to try to avoid having this required to run on the main thread (follow a similar approach to the 'SyncPushTokensJob' & `Atomic`?) private func registerForVanillaPushToken() -> AnyPublisher { - AssertIsOnMainThread() - // Use the existing publisher if it exists if let vanillaTokenPublisher: AnyPublisher = self.vanillaTokenPublisher { return vanillaTokenPublisher @@ -139,19 +138,23 @@ public enum PushRegistrationError: Error { .eraseToAnyPublisher() } - UIApplication.shared.registerForRemoteNotifications() - // No pending vanilla token yet; create a new publisher let publisher: AnyPublisher = Deferred { - Future { self.vanillaTokenResolver = $0 } + Future { + self.vanillaTokenResolver = $0 + + // Tell the device to register for remote notifications + DispatchQueue.main.sync { UIApplication.shared.registerForRemoteNotifications() } + } } + .shareReplay(1) .eraseToAnyPublisher() self.vanillaTokenPublisher = publisher return publisher .timeout( .seconds(10), - scheduler: DispatchQueue.main, + scheduler: DispatchQueue.global(qos: .default), customError: { PushRegistrationError.timeout } ) .catch { error -> AnyPublisher in @@ -200,9 +203,8 @@ public enum PushRegistrationError: Error { } public func createVoipRegistryIfNecessary() { - AssertIsOnMainThread() - guard voipRegistry == nil else { return } + let voipRegistry = PKPushRegistry(queue: nil) self.voipRegistry = voipRegistry voipRegistry.desiredPushTypes = [.voIP] @@ -210,8 +212,6 @@ public enum PushRegistrationError: Error { } private func registerForVoipPushToken() -> AnyPublisher { - AssertIsOnMainThread() - // Use the existing publisher if it exists if let voipTokenPublisher: AnyPublisher = self.voipTokenPublisher { return voipTokenPublisher diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index ba1a0a5f2..fac7107fa 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -31,59 +31,39 @@ public enum SyncPushTokensJob: JobExecutor { return deferred(job, dependencies) } - // We need to check a UIApplication setting which needs to run on the main thread so synchronously - // retrieve the value so we can continue - let isRegisteredForRemoteNotifications: Bool = { - guard !Thread.isMainThread else { - return UIApplication.shared.isRegisteredForRemoteNotifications - } - - return DispatchQueue.main.sync { - return UIApplication.shared.isRegisteredForRemoteNotifications - } - }() - - // Apple's documentation states that we should re-register for notifications on every launch: - // https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1 - guard job.behaviour == .runOnce || !isRegisteredForRemoteNotifications else { - SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled") - deferred(job, dependencies) // Don't need to do anything if push notifications are already registered - return - } - // Determine if the device has 'Fast Mode' (APNS) enabled let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs] // If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing // token guard isUsingFullAPNs else { - Just(Storage.shared[.lastRecordedPushToken]) + Just(dependencies.storage[.lastRecordedPushToken]) .setFailureType(to: Error.self) - .flatMap { lastRecordedPushToken in + .flatMap { lastRecordedPushToken -> AnyPublisher in + // Tell the device to unregister for remote notifications (essentially try to invalidate + // the token if needed - we do this first to avoid wrid race conditions which could be + // triggered by the user immediately re-registering) + DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() } + + // Clear the old token + dependencies.storage.write(using: dependencies) { db in + db[.lastRecordedPushToken] = nil + } + + // Unregister from our server if let existingToken: String = lastRecordedPushToken { SNLog("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))") - return Just(existingToken) - .setFailureType(to: Error.self) + return PushNotificationAPI.unsubscribe(token: Data(hex: existingToken)) + .map { _ in () } .eraseToAnyPublisher() } - SNLog("[SyncPushTokensJob] Unregister using live token provided from device") - return PushRegistrationManager.shared.requestPushTokens() - .map { token, _ in token } + SNLog("[SyncPushTokensJob] No previous token stored just triggering device unregister") + return Just(()) + .setFailureType(to: Error.self) .eraseToAnyPublisher() } - .flatMap { pushToken in PushNotificationAPI.unregister(Data(hex: pushToken)) } - .map { - // Tell the device to unregister for remote notifications (essentially try to invalidate - // the token if needed - DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() } - - Storage.shared.write { db in - db[.lastRecordedPushToken] = nil - } - return () - } - .subscribe(on: queue) + .subscribe(on: queue, using: dependencies) .sinkUntilComplete( receiveCompletion: { result in switch result { @@ -98,17 +78,20 @@ public enum SyncPushTokensJob: JobExecutor { return } - // Perform device registration + /// Perform device registration + /// + /// **Note:** Apple's documentation states that we should re-register for notifications on every launch: + /// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1 Logger.info("Re-registering for remote notifications.") PushRegistrationManager.shared.requestPushTokens() .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher in PushNotificationAPI - .register( - with: Data(hex: pushToken), - publicKey: getUserHexEncodedPublicKey(), - isForcedUpdate: true + .subscribe( + token: Data(hex: pushToken), + isForcedUpdate: true, + using: dependencies ) - .retry(3) + .retry(3, using: dependencies) .handleEvents( receiveCompletion: { result in switch result { @@ -118,9 +101,9 @@ public enum SyncPushTokensJob: JobExecutor { case .finished: Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") SNLog("[SyncPushTokensJob] Completed") - UserDefaults.standard[.lastPushNotificationSync] = Date() + dependencies.standardUserDefaults[.lastPushNotificationSync] = dependencies.dateNow - Storage.shared.write { db in + dependencies.storage.write(using: dependencies) { db in db[.lastRecordedPushToken] = pushToken db[.lastRecordedVoipToken] = voipToken } @@ -130,7 +113,7 @@ public enum SyncPushTokensJob: JobExecutor { .map { _ in () } .eraseToAnyPublisher() } - .subscribe(on: queue) + .subscribe(on: queue, using: dependencies) .sinkUntilComplete( // We want to complete this job regardless of success or failure receiveCompletion: { _ in success(job, false, dependencies) } @@ -168,5 +151,9 @@ extension SyncPushTokensJob { // MARK: - Convenience private func redact(_ string: String) -> String { - return OWSIsDebugBuild() ? string : "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]" +#if DEBUG + return string +#else + return "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]" +#endif } diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index 15c14a53a..383d1877f 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -6,6 +6,7 @@ import UserNotifications import SessionMessagingKit import SignalCoreKit import SignalUtilitiesKit +import SessionUtilitiesKit class UserNotificationConfig { diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index f76c7989d..bd462df81 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -190,9 +190,9 @@ enum Onboarding { // Notify the app that registration is complete Identity.didRegister() - // Now that we have registered get the Snode pool and sync push tokens + // Now that we have registered get the Snode pool (just in case) - other non-blocking + // launch jobs will automatically be run because the app activation was triggered GetSnodePoolJob.run() - SyncPushTokensJob.run(uploadOnlyIfStale: false) } } } diff --git a/Session/Onboarding/PNModeVC.swift b/Session/Onboarding/PNModeVC.swift index bf4884a29..c389e7b9b 100644 --- a/Session/Onboarding/PNModeVC.swift +++ b/Session/Onboarding/PNModeVC.swift @@ -6,6 +6,7 @@ import SessionUIKit import SessionMessagingKit import SessionSnodeKit import SignalUtilitiesKit +import SessionUtilitiesKit final class PNModeVC: BaseVC, OptionViewDelegate { private let flow: Onboarding.Flow diff --git a/Session/Onboarding/RegisterVC.swift b/Session/Onboarding/RegisterVC.swift index 52cc441a6..ef9cb8228 100644 --- a/Session/Onboarding/RegisterVC.swift +++ b/Session/Onboarding/RegisterVC.swift @@ -4,6 +4,7 @@ import UIKit import Sodium import SessionUIKit import SignalUtilitiesKit +import SessionUtilitiesKit final class RegisterVC : BaseVC { private var seed: Data! { didSet { updateKeyPair() } } diff --git a/Session/Settings/AppearanceViewController.swift b/Session/Settings/AppearanceViewController.swift index 10336d3ed..cca4a0b8f 100644 --- a/Session/Settings/AppearanceViewController.swift +++ b/Session/Settings/AppearanceViewController.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SignalUtilitiesKit +import SessionUtilitiesKit final class AppearanceViewController: BaseVC { // MARK: - Components diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index 871bae502..bf4a46bea 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -6,6 +6,7 @@ import GRDB import DifferenceKit import SessionUIKit import SignalUtilitiesKit +import SessionUtilitiesKit class BlockedContactsViewModel: SessionTableViewModel { // MARK: - Section diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index 1c6803913..5cf82d94e 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -33,6 +33,11 @@ class ConversationSettingsViewModel: SessionTableViewModel [SectionModel] in + .trackingConstantRegion { [weak self] db -> State in + State( + trimOpenGroupMessagesOlderThanSixMonths: db[.trimOpenGroupMessagesOlderThanSixMonths], + shouldAutoPlayConsecutiveAudioMessages: db[.shouldAutoPlayConsecutiveAudioMessages] + ) + } + .removeDuplicates() + .handleEvents(didFail: { SNLog("[ConversationSettingsViewModel] Observation failed with error: \($0)") }) + .publisher(in: Storage.shared) + .withPrevious() + .map { (previous: State?, current: State) -> [SectionModel] in return [ SectionModel( model: .messageTrimming, @@ -55,7 +70,11 @@ class ConversationSettingsViewModel: SessionTableViewModel Void diff --git a/Session/Settings/NotificationSettingsViewModel.swift b/Session/Settings/NotificationSettingsViewModel.swift index 8c0221165..046d0dfa3 100644 --- a/Session/Settings/NotificationSettingsViewModel.swift +++ b/Session/Settings/NotificationSettingsViewModel.swift @@ -7,7 +7,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -class NotificationSettingsViewModel: SessionTableViewModel { +class NotificationSettingsViewModel: SessionTableViewModel { // MARK: - Config public enum Section: SessionTableSection { @@ -31,7 +31,7 @@ class NotificationSettingsViewModel: SessionTableViewModel [SectionModel] in - let notificationSound: Preferences.Sound = db[.defaultNotificationSound] - .defaulting(to: Preferences.Sound.defaultNotificationSound) - let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] - .defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType) - + .trackingConstantRegion { db -> State in + State( + isUsingFullAPNs: false, // Set later the the data flow + notificationSound: db[.defaultNotificationSound] + .defaulting(to: Preferences.Sound.defaultNotificationSound), + playNotificationSoundInForeground: db[.playNotificationSoundInForeground], + previewType: db[.preferencesNotificationPreviewType] + .defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType) + ) + } + .removeDuplicates() + .handleEvents(didFail: { SNLog("[NotificationSettingsViewModel] Observation failed with error: \($0)") }) + .publisher(in: Storage.shared) + .manualRefreshFrom(forcedRefresh) + .map { dbState -> State in + State( + isUsingFullAPNs: UserDefaults.standard[.isUsingFullAPNs], + notificationSound: dbState.notificationSound, + playNotificationSoundInForeground: dbState.playNotificationSoundInForeground, + previewType: dbState.previewType + ) + } + .withPrevious() + .map { (previous: State?, current: State) -> [SectionModel] in return [ SectionModel( model: .strategy, @@ -68,20 +93,24 @@ class NotificationSettingsViewModel: SessionTableViewModel [SectionModel] in + .trackingConstantRegion { [weak self] db -> State in + State( + isScreenLockEnabled: db[.isScreenLockEnabled], + checkForCommunityMessageRequests: db[.checkForCommunityMessageRequests], + areReadReceiptsEnabled: db[.areReadReceiptsEnabled], + typingIndicatorsEnabled: db[.typingIndicatorsEnabled], + areLinkPreviewsEnabled: db[.areLinkPreviewsEnabled], + areCallsEnabled: db[.areCallsEnabled] + ) + } + .removeDuplicates() + .handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") }) + .publisher(in: Storage.shared) + .withPrevious() + .map { (previous: State?, current: State) -> [SectionModel] in return [ SectionModel( model: .screenSecurity, @@ -96,7 +122,13 @@ class PrivacySettingsViewModel: SessionTableViewModel 100 } // Prevent too many changes from causing performance issues ) { [weak self] updatedData in self?.viewModel.updateTableData(updatedData) @@ -339,6 +339,7 @@ class SessionTableViewController { Just(nil).eraseToAnyPublisher() } open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() } + private let _forcedRefresh: PassthroughSubject = PassthroughSubject() + lazy var forcedRefresh: AnyPublisher = _forcedRefresh + .shareReplay(0) private let _showToast: PassthroughSubject<(String, ThemeValue), Never> = PassthroughSubject() lazy var showToast: AnyPublisher<(String, ThemeValue), Never> = _showToast .shareReplay(0) @@ -62,6 +65,10 @@ class SessionTableViewModel( for viewModel: SessionTableViewModel ) -> AnyPublisher<(Output, StagedChangeset), Failure> where Output == [ArraySection>] { diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index af9d617eb..4bacade56 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -394,19 +394,30 @@ extension SessionCell.Accessory { extension SessionCell.Accessory { public enum DataSource: Hashable, Equatable { - case boolValue(Bool) + case boolValue(key: String, value: Bool, oldValue: Bool) case dynamicString(() -> String?) - case userDefaults(UserDefaults, key: String) - case settingBool(key: Setting.BoolKey) + + static func boolValue(_ value: Bool, oldValue: Bool) -> DataSource { + return .boolValue(key: "", value: value, oldValue: oldValue) + } + + static func boolValue(key: Setting.BoolKey, value: Bool, oldValue: Bool) -> DataSource { + return .boolValue(key: key.rawValue, value: value, oldValue: oldValue) + } // MARK: - Convenience public var currentBoolValue: Bool { switch self { - case .boolValue(let value): return value + case .boolValue(_, let value, _): return value case .dynamicString: return false - case .userDefaults(let defaults, let key): return defaults.bool(forKey: key) - case .settingBool(let key): return Storage.shared[key] + } + } + + public var oldBoolValue: Bool { + switch self { + case .boolValue(_, _, let oldValue): return oldValue + default: return false } } @@ -421,27 +432,27 @@ extension SessionCell.Accessory { public func hash(into hasher: inout Hasher) { switch self { - case .boolValue(let value): value.hash(into: &hasher) + case .boolValue(let key, let value, let oldValue): + key.hash(into: &hasher) + value.hash(into: &hasher) + oldValue.hash(into: &hasher) + case .dynamicString(let generator): generator().hash(into: &hasher) - case .userDefaults(_, let key): key.hash(into: &hasher) - case .settingBool(let key): key.hash(into: &hasher) } } public static func == (lhs: DataSource, rhs: DataSource) -> Bool { switch (lhs, rhs) { - case (.boolValue(let lhsValue), .boolValue(let rhsValue)): - return (lhsValue == rhsValue) + case (.boolValue(let lhsKey, let lhsValue, let lhsOldValue), .boolValue(let rhsKey, let rhsValue, let rhsOldValue)): + return ( + lhsKey == rhsKey && + lhsValue == rhsValue && + lhsOldValue == rhsOldValue + ) case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)): return (lhsGenerator() == rhsGenerator()) - case (.userDefaults(_, let lhsKey), .userDefaults(_, let rhsKey)): - return (lhsKey == rhsKey) - - case (.settingBool(let lhsKey), .settingBool(let rhsKey)): - return (lhsKey == rhsKey) - default: return false } } diff --git a/Session/Shared/Types/SessionCell+Styling.swift b/Session/Shared/Types/SessionCell+Styling.swift index 948cd1631..e7a454388 100644 --- a/Session/Shared/Types/SessionCell+Styling.swift +++ b/Session/Shared/Types/SessionCell+Styling.swift @@ -1,6 +1,6 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -import Foundation +import UIKit import SessionUIKit // MARK: - Main Types diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 44c81b9eb..39ca4344a 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -277,7 +277,8 @@ extension SessionCell { public func update( with accessory: Accessory?, tintColor: ThemeValue, - isEnabled: Bool + isEnabled: Bool, + isManualReload: Bool ) { guard let accessory: Accessory = accessory else { return } @@ -356,10 +357,15 @@ extension SessionCell { fixedWidthConstraint.isActive = true toggleSwitchConstraints.forEach { $0.isActive = true } - let newValue: Bool = dataSource.currentBoolValue - - if newValue != toggleSwitch.isOn { - toggleSwitch.setOn(newValue, animated: true) + if !isManualReload { + toggleSwitch.setOn(dataSource.oldBoolValue, animated: false) + + // Dispatch so the cell reload doesn't conflict with the setting change animation + if dataSource.oldBoolValue != dataSource.currentBoolValue { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak toggleSwitch] in + toggleSwitch?.setOn(dataSource.currentBoolValue, animated: true) + } + } } case .dropDown(let dataSource, let accessibility): diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 912fb37a9..1b2b8630e 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -313,7 +313,7 @@ public class SessionCell: UITableViewCell { botSeparator.isHidden = true } - public func update(with info: Info) { + public func update(with info: Info, isManualReload: Bool = false) { interactionMode = (info.title?.interaction ?? .none) shouldHighlightTitle = (info.title?.interaction != .copy) titleExtraView = info.title?.extraViewGenerator?() @@ -332,7 +332,8 @@ public class SessionCell: UITableViewCell { leftAccessoryView.update( with: info.leftAccessory, tintColor: info.styling.tintColor, - isEnabled: info.isEnabled + isEnabled: info.isEnabled, + isManualReload: isManualReload ) titleStackView.isHidden = (info.title == nil && info.subtitle == nil) titleLabel.isUserInteractionEnabled = (info.title?.interaction == .copy) @@ -356,7 +357,8 @@ public class SessionCell: UITableViewCell { rightAccessoryView.update( with: info.rightAccessory, tintColor: info.styling.tintColor, - isEnabled: info.isEnabled + isEnabled: info.isEnabled, + isManualReload: isManualReload ) contentStackViewLeadingConstraint.isActive = (info.styling.alignment == .leading) diff --git a/Session/Utilities/Date+Utilities.swift b/Session/Utilities/Date+Utilities.swift index ed7aab4f4..c6e440f3f 100644 --- a/Session/Utilities/Date+Utilities.swift +++ b/Session/Utilities/Date+Utilities.swift @@ -5,6 +5,9 @@ import SessionUtilitiesKit public extension Date { var formattedForDisplay: String { + // If we don't have a date then + guard self.timeIntervalSince1970 > 0 else { return "" } + let dateNow: Date = Date() guard Calendar.current.isDate(self, equalTo: dateNow, toGranularity: .year) else { diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index b27698f5c..326afc83e 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -1,6 +1,7 @@ import Foundation import GRDB import SessionSnodeKit +import SessionUtilitiesKit final class IP2Country { static var isInitialized = false @@ -12,16 +13,16 @@ final class IP2Country { /// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding /// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking /// up (converted to an integer). The IP we're looking up must then be in the range **before** that range. - private lazy var ipv4Table: [String:[Int]] = { + private lazy var ipv4Table: [String: [Int]] = { let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)! let data = try! Data(contentsOf: url) - return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:[Int]] + return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String: [Int]] }() - private lazy var countryNamesTable: [String:[String]] = { + private lazy var countryNamesTable: [String: [String]] = { let url = Bundle.main.url(forResource: "GeoLite2-Country-Locations-English", withExtension: nil)! let data = try! Data(contentsOf: url) - return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:[String]] + return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String: [String]] }() // MARK: Lifecycle diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index 1cbf94fbe..e41c9be22 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import Curve25519Kit import SessionMessagingKit +import SessionUtilitiesKit enum MockDataGenerator { // MARK: - Generation @@ -99,7 +100,8 @@ enum MockDataGenerator { .compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) } .joined(), lastNameUpdate: Date().timeIntervalSince1970, - lastProfilePictureUpdate: Date().timeIntervalSince1970 + lastProfilePictureUpdate: Date().timeIntervalSince1970, + lastBlocksCommunityMessageRequests: 0 ) .saved(db) @@ -180,7 +182,8 @@ enum MockDataGenerator { .compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) } .joined(), lastNameUpdate: Date().timeIntervalSince1970, - lastProfilePictureUpdate: Date().timeIntervalSince1970 + lastProfilePictureUpdate: Date().timeIntervalSince1970, + lastBlocksCommunityMessageRequests: 0 ) .saved(db) @@ -310,7 +313,8 @@ enum MockDataGenerator { .compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) } .joined(), lastNameUpdate: Date().timeIntervalSince1970, - lastProfilePictureUpdate: Date().timeIntervalSince1970 + lastProfilePictureUpdate: Date().timeIntervalSince1970, + lastBlocksCommunityMessageRequests: 0 ) .saved(db) diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index 1d33a47c6..85997e42a 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -3,6 +3,7 @@ import UIKit import SessionMessagingKit import SessionUIKit +import SessionUtilitiesKit protocol SwipeActionOptimisticCell { func optimisticUpdate(isMuted: Bool?, isBlocked: Bool?, isPinned: Bool?, hasUnread: Bool?) diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 314a5ca7e..2644c73ec 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -31,7 +31,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API _011_AddPendingReadReceipts.self, _012_AddFTSIfNeeded.self, _013_SessionUtilChanges.self, - _014_GenerateInitialUserConfigDumps.self + _014_GenerateInitialUserConfigDumps.self, + _015_BlockCommunityMessageRequests.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index da9e1544e..e8c1d43a7 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -14,7 +14,6 @@ enum _003_YDBToGRDBMigration: Migration { static func migrate(_ db: Database) throws { guard !SNUtilitiesKit.isRunningTests else { return Storage.update(progress: 1, for: self, in: target) } - SNLogNotTests("[Migration Error] Attempted to perform legacy migation") throw StorageError.migrationNoLongerSupported } diff --git a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift b/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift index 65e68507c..235a217d3 100644 --- a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift +++ b/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift @@ -9,7 +9,7 @@ enum _005_FixDeletedMessageReadState: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "FixDeletedMessageReadState" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { _ = try Interaction diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift index c1097eb94..b746c6362 100644 --- a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift +++ b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift @@ -10,7 +10,7 @@ enum _006_FixHiddenModAdminSupport: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "FixHiddenModAdminSupport" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.alter(table: GroupMember.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift b/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift index b468098f7..5e53bb6ee 100644 --- a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift +++ b/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift @@ -9,7 +9,7 @@ enum _007_HomeQueryOptimisationIndexes: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "HomeQueryOptimisationIndexes" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.create( diff --git a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift b/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift index b06687dca..399dba483 100644 --- a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift +++ b/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift @@ -9,7 +9,7 @@ enum _008_EmojiReacts: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "EmojiReacts" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.create(table: Reaction.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift b/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift index 4f6036a2d..f4c7e8617 100644 --- a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift +++ b/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift @@ -8,7 +8,7 @@ enum _009_OpenGroupPermission: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "OpenGroupPermission" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: GRDB.Database) throws { try db.alter(table: OpenGroup.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift b/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift index 9c2e228d5..2fb57b2cf 100644 --- a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift +++ b/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift @@ -10,7 +10,7 @@ enum _011_AddPendingReadReceipts: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "AddPendingReadReceipts" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.create(table: PendingReadReceipt.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift b/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift index d994b6a90..57cb66e7d 100644 --- a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift +++ b/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift @@ -9,7 +9,7 @@ enum _012_AddFTSIfNeeded: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "AddFTSIfNeeded" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { // Fix an issue that the fullTextSearchTable was dropped unintentionally and global search won't work. diff --git a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift b/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift new file mode 100644 index 000000000..b512101b2 --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift @@ -0,0 +1,44 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// This migration adds a flag indicating whether a profile has indicated it is blocking community message requests +enum _015_BlockCommunityMessageRequests: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "BlockCommunityMessageRequests" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.01 + static var requirements: [MigrationRequirement] = [.sessionUtilStateLoaded] + + static func migrate(_ db: Database) throws { + // Add the new 'Profile' properties + try db.alter(table: Profile.self) { t in + t.add(.blocksCommunityMessageRequests, .boolean) + t.add(.lastBlocksCommunityMessageRequests, .integer) + .notNull() + .defaults(to: 0) + } + + // If the user exists and the 'checkForCommunityMessageRequests' hasn't already been set then default it to "false" + if + Identity.userExists(db), + (try Setting.exists(db, id: Setting.BoolKey.checkForCommunityMessageRequests.rawValue)) == false + { + let rawBlindedMessageRequestValue: Int32 = try SessionUtil + .config(for: .userProfile, publicKey: getUserHexEncodedPublicKey(db)) + .wrappedValue + .map { conf -> Int32 in try SessionUtil.rawBlindedMessageRequestValue(in: conf) } + .defaulting(to: -1) + + // Use the value in the config if we happen to have one, otherwise use the default + db[.checkForCommunityMessageRequests] = (rawBlindedMessageRequestValue < 0 ? + true : + (rawBlindedMessageRequestValue > 0) + ) + } + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 43e922ed0..de285bb2b 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -144,10 +144,9 @@ public extension ClosedGroup { ClosedGroupPoller.shared.stopPolling(for: threadId) PushNotificationAPI - .performOperation( - .unsubscribe, - for: threadId, - publicKey: userPublicKey + .unsubscribeFromLegacyGroup( + legacyGroupId: threadId, + currentUserPublicKey: userPublicKey ) .sinkUntilComplete() } diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index cc4e4bef6..a9a6bd8af 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -27,6 +27,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco case profilePictureFileName case profileEncryptionKey case lastProfilePictureUpdate + + case blocksCommunityMessageRequests + case lastBlocksCommunityMessageRequests } /// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant) @@ -53,6 +56,12 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco /// The timestamp (in seconds since epoch) that the profile picture was last updated public let lastProfilePictureUpdate: TimeInterval + /// A flag indicating whether this profile has reported that it blocks community message requests + public let blocksCommunityMessageRequests: Bool? + + /// The timestamp (in seconds since epoch) that the `blocksCommunityMessageRequests` setting was last updated + public let lastBlocksCommunityMessageRequests: TimeInterval + // MARK: - Initialization public init( @@ -63,7 +72,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco profilePictureUrl: String? = nil, profilePictureFileName: String? = nil, profileEncryptionKey: Data? = nil, - lastProfilePictureUpdate: TimeInterval + lastProfilePictureUpdate: TimeInterval, + blocksCommunityMessageRequests: Bool? = nil, + lastBlocksCommunityMessageRequests: TimeInterval ) { self.id = id self.name = name @@ -73,6 +84,8 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco self.profilePictureFileName = profilePictureFileName self.profileEncryptionKey = profileEncryptionKey self.lastProfilePictureUpdate = lastProfilePictureUpdate + self.blocksCommunityMessageRequests = blocksCommunityMessageRequests + self.lastBlocksCommunityMessageRequests = lastBlocksCommunityMessageRequests } // MARK: - Description @@ -114,7 +127,9 @@ public extension Profile { profilePictureUrl: profilePictureUrl, profilePictureFileName: try? container.decode(String.self, forKey: .profilePictureFileName), profileEncryptionKey: profileKey, - lastProfilePictureUpdate: try container.decode(TimeInterval.self, forKey: .lastProfilePictureUpdate) + lastProfilePictureUpdate: try container.decode(TimeInterval.self, forKey: .lastProfilePictureUpdate), + blocksCommunityMessageRequests: try? container.decode(Bool.self, forKey: .blocksCommunityMessageRequests), + lastBlocksCommunityMessageRequests: try container.decode(TimeInterval.self, forKey: .lastBlocksCommunityMessageRequests) ) } @@ -129,6 +144,8 @@ public extension Profile { try container.encodeIfPresent(profilePictureFileName, forKey: .profilePictureFileName) try container.encodeIfPresent(profileEncryptionKey, forKey: .profileEncryptionKey) try container.encode(lastProfilePictureUpdate, forKey: .lastProfilePictureUpdate) + try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests) + try container.encode(lastBlocksCommunityMessageRequests, forKey: .lastBlocksCommunityMessageRequests) } } @@ -156,7 +173,9 @@ public extension Profile { profilePictureUrl: profilePictureUrl, profilePictureFileName: nil, profileEncryptionKey: profileKey, - lastProfilePictureUpdate: sentTimestamp + lastProfilePictureUpdate: sentTimestamp, + blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil), + lastBlocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? sentTimestamp : 0) ) } @@ -242,7 +261,9 @@ public extension Profile { profilePictureUrl: nil, profilePictureFileName: nil, profileEncryptionKey: nil, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + blocksCommunityMessageRequests: nil, + lastBlocksCommunityMessageRequests: 0 ) } diff --git a/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift b/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift index 01500f714..a97101bab 100644 --- a/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift +++ b/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift @@ -5,6 +5,7 @@ import Combine import SessionSnodeKit import SessionUtilitiesKit +// FIXME: Remove this once legacy notifications and legacy groups are deprecated public enum NotifyPushServerJob: JobExecutor { public static var maxFailureCount: Int = 20 public static var requiresThreadId: Bool = false @@ -27,7 +28,7 @@ public enum NotifyPushServerJob: JobExecutor { } PushNotificationAPI - .notify( + .legacyNotify( recipient: details.message.recipient, with: details.message.data, maxRetryCount: 4 diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index 8f63ed5a9..4ad3649ac 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -10,15 +10,22 @@ public extension VisibleMessage { public let displayName: String? public let profileKey: Data? public let profilePictureUrl: String? + public let blocksCommunityMessageRequests: Bool? // MARK: - Initialization - internal init(displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil) { + internal init( + displayName: String, + profileKey: Data? = nil, + profilePictureUrl: String? = nil, + blocksCommunityMessageRequests: Bool? = nil + ) { let hasUrlAndKey: Bool = (profileKey != nil && profilePictureUrl != nil) self.displayName = displayName self.profileKey = (hasUrlAndKey ? profileKey : nil) self.profilePictureUrl = (hasUrlAndKey ? profilePictureUrl : nil) + self.blocksCommunityMessageRequests = blocksCommunityMessageRequests } // MARK: - Proto Conversion @@ -32,7 +39,8 @@ public extension VisibleMessage { return VMProfile( displayName: displayName, profileKey: proto.profileKey, - profilePictureUrl: profileProto.profilePicture + profilePictureUrl: profileProto.profilePicture, + blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil) ) } @@ -45,6 +53,10 @@ public extension VisibleMessage { let profileProto = SNProtoLokiProfile.builder() profileProto.setDisplayName(displayName) + if let blocksCommunityMessageRequests: Bool = self.blocksCommunityMessageRequests { + dataMessageProto.setBlocksCommunityMessageRequests(blocksCommunityMessageRequests) + } + if let profileKey = profileKey, let profilePictureUrl = profilePictureUrl { dataMessageProto.setProfileKey(profileKey) profileProto.setProfilePicture(profilePictureUrl) @@ -112,10 +124,14 @@ public extension VisibleMessage { // MARK: - Conversion extension VisibleMessage.VMProfile { - init(profile: Profile) { + init( + profile: Profile, + blocksCommunityMessageRequests: Bool? + ) { self.displayName = profile.name self.profileKey = profile.profileEncryptionKey self.profilePictureUrl = profile.profilePictureUrl + self.blocksCommunityMessageRequests = blocksCommunityMessageRequests } } diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 7adecefea..4e3b69091 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -97,7 +97,7 @@ extension OpenGroupAPI.Message { throw HTTPError.parsingFailed } - case .none: + case .none, .group: SNLog("Ignoring message with invalid sender.") throw HTTPError.parsingFailed } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 4cd74f821..5c5d622c0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -109,10 +109,12 @@ public enum OpenGroupAPI { // The 'inbox' and 'outbox' only work with blinded keys so don't bother polling them if not blinded !capabilities.contains(.blind) ? [] : [ - // Inbox - (lastInboxMessageId == 0 ? - try preparedInbox(db, on: server, using: dependencies) : - try preparedInboxSince(db, id: lastInboxMessageId, on: server, using: dependencies) + // Inbox (only check the inbox if the user want's community message requests) + (!db[.checkForCommunityMessageRequests] ? nil : + (lastInboxMessageId == 0 ? + try preparedInbox(db, on: server, using: dependencies) : + try preparedInboxSince(db, id: lastInboxMessageId, on: server, using: dependencies) + ) ), // Outbox @@ -120,7 +122,7 @@ public enum OpenGroupAPI { try preparedOutbox(db, on: server, using: dependencies) : try preparedOutboxSince(db, id: lastOutboxMessageId, on: server, using: dependencies) ), - ] + ].compactMap { $0 } ) ) diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 25b6c93df..1af88d959 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -976,6 +976,8 @@ public final class OpenGroupManager { .filter(possibleKeys.contains(GroupMember.Columns.profileId)) .filter(targetRoles.contains(GroupMember.Columns.role)) .isNotEmpty(db) + + case .group: return false } } .defaulting(to: false) diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index ed766ca8d..72e4ca517 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -2497,6 +2497,9 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr if let _value = syncTarget { builder.setSyncTarget(_value) } + if hasBlocksCommunityMessageRequests { + builder.setBlocksCommunityMessageRequests(blocksCommunityMessageRequests) + } return builder } @@ -2570,6 +2573,10 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr proto.syncTarget = valueParam } + @objc public func setBlocksCommunityMessageRequests(_ valueParam: Bool) { + proto.blocksCommunityMessageRequests = valueParam + } + @objc public func build() throws -> SNProtoDataMessage { return try SNProtoDataMessage.parseProto(proto) } @@ -2646,6 +2653,13 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr return proto.hasSyncTarget } + @objc public var blocksCommunityMessageRequests: Bool { + return proto.blocksCommunityMessageRequests + } + @objc public var hasBlocksCommunityMessageRequests: Bool { + return proto.hasBlocksCommunityMessageRequests + } + private init(proto: SessionProtos_DataMessage, attachments: [SNProtoAttachmentPointer], quote: SNProtoDataMessageQuote?, diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index 6f209cb67..3fb72c9ad 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -600,7 +600,7 @@ struct SessionProtos_DataMessage { set {_uniqueStorage()._attachments = newValue} } - /// optional GroupContext group = 3; // No longer used + /// optional GroupContext group = 3; // No longer used var flags: UInt32 { get {return _storage._flags ?? 0} set {_uniqueStorage()._flags = newValue} @@ -696,6 +696,15 @@ struct SessionProtos_DataMessage { /// Clears the value of `syncTarget`. Subsequent reads from it will return its default value. mutating func clearSyncTarget() {_uniqueStorage()._syncTarget = nil} + var blocksCommunityMessageRequests: Bool { + get {return _storage._blocksCommunityMessageRequests ?? false} + set {_uniqueStorage()._blocksCommunityMessageRequests = newValue} + } + /// Returns true if `blocksCommunityMessageRequests` has been explicitly set. + var hasBlocksCommunityMessageRequests: Bool {return _storage._blocksCommunityMessageRequests != nil} + /// Clears the value of `blocksCommunityMessageRequests`. Subsequent reads from it will return its default value. + mutating func clearBlocksCommunityMessageRequests() {_uniqueStorage()._blocksCommunityMessageRequests = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() enum Flags: SwiftProtobuf.Enum { @@ -1665,6 +1674,43 @@ extension SessionProtos_SharedConfigMessage.Kind: CaseIterable { #endif // swift(>=4.2) +#if swift(>=5.5) && canImport(_Concurrency) +extension SessionProtos_Envelope: @unchecked Sendable {} +extension SessionProtos_Envelope.TypeEnum: @unchecked Sendable {} +extension SessionProtos_TypingMessage: @unchecked Sendable {} +extension SessionProtos_TypingMessage.Action: @unchecked Sendable {} +extension SessionProtos_UnsendRequest: @unchecked Sendable {} +extension SessionProtos_MessageRequestResponse: @unchecked Sendable {} +extension SessionProtos_Content: @unchecked Sendable {} +extension SessionProtos_CallMessage: @unchecked Sendable {} +extension SessionProtos_CallMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_KeyPair: @unchecked Sendable {} +extension SessionProtos_DataExtractionNotification: @unchecked Sendable {} +extension SessionProtos_DataExtractionNotification.TypeEnum: @unchecked Sendable {} +extension SessionProtos_LokiProfile: @unchecked Sendable {} +extension SessionProtos_DataMessage: @unchecked Sendable {} +extension SessionProtos_DataMessage.Flags: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote.QuotedAttachment: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: @unchecked Sendable {} +extension SessionProtos_DataMessage.Preview: @unchecked Sendable {} +extension SessionProtos_DataMessage.Reaction: @unchecked Sendable {} +extension SessionProtos_DataMessage.Reaction.Action: @unchecked Sendable {} +extension SessionProtos_DataMessage.OpenGroupInvitation: @unchecked Sendable {} +extension SessionProtos_DataMessage.ClosedGroupControlMessage: @unchecked Sendable {} +extension SessionProtos_DataMessage.ClosedGroupControlMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_DataMessage.ClosedGroupControlMessage.KeyPairWrapper: @unchecked Sendable {} +extension SessionProtos_ConfigurationMessage: @unchecked Sendable {} +extension SessionProtos_ConfigurationMessage.ClosedGroup: @unchecked Sendable {} +extension SessionProtos_ConfigurationMessage.Contact: @unchecked Sendable {} +extension SessionProtos_ReceiptMessage: @unchecked Sendable {} +extension SessionProtos_ReceiptMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_AttachmentPointer: @unchecked Sendable {} +extension SessionProtos_AttachmentPointer.Flags: @unchecked Sendable {} +extension SessionProtos_SharedConfigMessage: @unchecked Sendable {} +extension SessionProtos_SharedConfigMessage.Kind: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "SessionProtos" @@ -2288,6 +2334,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 102: .same(proto: "openGroupInvitation"), 104: .same(proto: "closedGroupControlMessage"), 105: .same(proto: "syncTarget"), + 106: .same(proto: "blocksCommunityMessageRequests"), ] fileprivate class _StorageClass { @@ -2304,6 +2351,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa var _openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation? = nil var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil var _syncTarget: String? = nil + var _blocksCommunityMessageRequests: Bool? = nil static let defaultInstance = _StorageClass() @@ -2323,6 +2371,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa _openGroupInvitation = source._openGroupInvitation _closedGroupControlMessage = source._closedGroupControlMessage _syncTarget = source._syncTarget + _blocksCommunityMessageRequests = source._blocksCommunityMessageRequests } } @@ -2366,6 +2415,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa case 102: try { try decoder.decodeSingularMessageField(value: &_storage._openGroupInvitation) }() case 104: try { try decoder.decodeSingularMessageField(value: &_storage._closedGroupControlMessage) }() case 105: try { try decoder.decodeSingularStringField(value: &_storage._syncTarget) }() + case 106: try { try decoder.decodeSingularBoolField(value: &_storage._blocksCommunityMessageRequests) }() default: break } } @@ -2417,6 +2467,9 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = _storage._syncTarget { try visitor.visitSingularStringField(value: v, fieldNumber: 105) } }() + try { if let v = _storage._blocksCommunityMessageRequests { + try visitor.visitSingularBoolField(value: v, fieldNumber: 106) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -2439,6 +2492,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if _storage._openGroupInvitation != rhs_storage._openGroupInvitation {return false} if _storage._closedGroupControlMessage != rhs_storage._closedGroupControlMessage {return false} if _storage._syncTarget != rhs_storage._syncTarget {return false} + if _storage._blocksCommunityMessageRequests != rhs_storage._blocksCommunityMessageRequests {return false} return true } if !storagesAreEqual {return false} diff --git a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift index 737e40ce6..2fe165044 100644 --- a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift +++ b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift @@ -218,6 +218,13 @@ extension WebSocketProtos_WebSocketMessage.TypeEnum: CaseIterable { #endif // swift(>=4.2) +#if swift(>=5.5) && canImport(_Concurrency) +extension WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketMessage.TypeEnum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "WebSocketProtos" diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 429c10b14..55d77b18f 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -192,20 +192,21 @@ message DataMessage { optional uint32 expirationTimer = 8; } - optional string body = 1; - repeated AttachmentPointer attachments = 2; - // optional GroupContext group = 3; // No longer used - optional uint32 flags = 4; - optional uint32 expireTimer = 5; - optional bytes profileKey = 6; - optional uint64 timestamp = 7; - optional Quote quote = 8; - repeated Preview preview = 10; - optional Reaction reaction = 11; - optional LokiProfile profile = 101; - optional OpenGroupInvitation openGroupInvitation = 102; - optional ClosedGroupControlMessage closedGroupControlMessage = 104; - optional string syncTarget = 105; + optional string body = 1; + repeated AttachmentPointer attachments = 2; + // optional GroupContext group = 3; // No longer used + optional uint32 flags = 4; + optional uint32 expireTimer = 5; + optional bytes profileKey = 6; + optional uint64 timestamp = 7; + optional Quote quote = 8; + repeated Preview preview = 10; + optional Reaction reaction = 11; + optional LokiProfile profile = 101; + optional OpenGroupInvitation openGroupInvitation = 102; + optional ClosedGroupControlMessage closedGroupControlMessage = 104; + optional string syncTarget = 105; + optional bool blocksCommunityMessageRequests = 106; } message ConfigurationMessage { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 22a28658c..409fd76cf 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -231,8 +231,24 @@ extension MessageReceiver { // Start polling ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey, using: dependencies) - // Notify the PN server - let _ = PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey(db)) + // Resubscribe for group push notifications + let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) + + PushNotificationAPI + .subscribeToLegacyGroups( + currentUserPublicKey: currentUserPublicKey, + legacyGroupIds: try ClosedGroup + .select(.threadId) + .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == currentUserPublicKey) + ) + .asRequest(of: String.self) + .fetchSet(db) + .inserting(groupPublicKey) // Insert the new key just to be sure + ) + .sinkUntilComplete() } /// Extracts and adds the new encryption key pair to our list of key pairs if there is one for our public key, AND the message was diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index a1a959306..520002125 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -31,6 +31,7 @@ extension MessageReceiver { db, publicKey: sender, name: profile.displayName, + blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, avatarUpdate: { guard let profilePictureUrl: String = profile.profilePictureUrl, @@ -73,7 +74,7 @@ extension MessageReceiver { return try? OpenGroup.fetchOne(db, id: threadId) }() - let variant: Interaction.Variant = { + let variant: Interaction.Variant = try { guard let senderSessionId: SessionId = SessionId(from: sender), let openGroup: OpenGroup = maybeOpenGroup @@ -115,6 +116,10 @@ extension MessageReceiver { .standardOutgoing : .standardIncoming ) + + case .group: + SNLog("Ignoring message with invalid sender.") + throw HTTPError.parsingFailed } }() diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index 19841fd95..c582fe56d 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -16,7 +16,7 @@ extension MessageSender { using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { dependencies.storage - .writePublisher { db -> (String, SessionThread, [MessageSender.PreparedSendData]) in + .writePublisher { db -> (String, SessionThread, [MessageSender.PreparedSendData], Set) in // Generate the group's two keys guard let groupKeyPair: KeyPair = dependencies.crypto.generate(.x25519KeyPair()), @@ -108,21 +108,30 @@ extension MessageSender { using: dependencies ) } + let allActiveLegacyGroupIds: Set = try ClosedGroup + .select(.threadId) + .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == userPublicKey) + ) + .asRequest(of: String.self) + .fetchSet(db) + .inserting(groupPublicKey) // Insert the new key just to be sure - return (userPublicKey, thread, memberSendData) + return (userPublicKey, thread, memberSendData, allActiveLegacyGroupIds) } - .flatMap { userPublicKey, thread, memberSendData in + .flatMap { userPublicKey, thread, memberSendData, allActiveLegacyGroupIds in Publishers .MergeMany( // Send a closed group update message to all members individually memberSendData .map { MessageSender.sendImmediate(data: $0, using: dependencies) } .appending( - // Notify the PN server - PushNotificationAPI.performOperation( - .subscribe, - for: thread.id, - publicKey: userPublicKey + // Resubscribe to all legacy groups + PushNotificationAPI.subscribeToLegacyGroups( + currentUserPublicKey: userPublicKey, + legacyGroupIds: allActiveLegacyGroupIds ) ) ) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 7072b8f7c..15855fd28 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -65,6 +65,11 @@ public enum MessageReceiver { userEd25519KeyPair: userEd25519KeyPair, using: dependencies ) + + case .group: + // TODO: Need to decide how we will handle updated group messages + SNLog("Ignoring message with invalid sender.") + throw HTTPError.parsingFailed } case .closedGroupMessage: diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 9b968bdab..747841f4e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -436,7 +436,8 @@ public final class MessageSender { // Attach the user's profile message.profile = VisibleMessage.VMProfile( - profile: Profile.fetchOrCreateCurrentUser() + profile: Profile.fetchOrCreateCurrentUser(db), + blocksCommunityMessageRequests: !db[.checkForCommunityMessageRequests] ) if (message.profile?.displayName ?? "").isEmpty { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift new file mode 100644 index 000000000..1a87dcf8e --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift @@ -0,0 +1,12 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct LegacyGroupOnlyRequest: Codable { + let token: String + let pubKey: String + let device: String + let legacyGroupPublicKeys: Set + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift new file mode 100644 index 000000000..962011dfd --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift @@ -0,0 +1,10 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct LegacyGroupRequest: Codable { + let pubKey: String + let closedGroupPublicKey: String + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift new file mode 100644 index 000000000..491fa7757 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift @@ -0,0 +1,15 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct LegacyNotifyRequest: Codable { + enum CodingKeys: String, CodingKey { + case data + case sendTo = "send_to" + } + + let data: String + let sendTo: String + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushServerResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift similarity index 78% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/PushServerResponse.swift rename to SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift index eee22e266..dc8c77ff7 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushServerResponse.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift @@ -3,7 +3,7 @@ import Foundation extension PushNotificationAPI { - struct PushServerResponse: Codable { + struct LegacyPushServerResponse: Codable { let code: Int let message: String? } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift new file mode 100644 index 000000000..663bafb17 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift @@ -0,0 +1,14 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionSnodeKit + +extension PushNotificationAPI { + struct LegacyUnsubscribeRequest: Codable { + private let token: String + + init(token: String) { + self.token = token + } + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift new file mode 100644 index 000000000..9a3633d85 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift @@ -0,0 +1,47 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct NotificationMetadata: Codable { + private enum CodingKeys: String, CodingKey { + case accountId = "@" + case hash = "#" + case namespace = "n" + case dataLength = "l" + case dataTooLong = "B" + } + + /// Account ID (such as Session ID or closed group ID) where the message arrived. + let accountId: String + + /// The hash of the message in the swarm. + let hash: String + + /// The swarm namespace in which this message arrived. + let namespace: Int + + /// The length of the message data. This is always included, even if the message content + /// itself was too large to fit into the push notification. + let dataLength: Int + + /// This will be `true` if the data was omitted because it was too long to fit in a push + /// notification (around 2.5kB of raw data), in which case the push notification includes + /// only this metadata but not the message content itself. + let dataTooLong: Bool + } +} + +extension PushNotificationAPI.NotificationMetadata { + init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = PushNotificationAPI.NotificationMetadata( + accountId: try container.decode(String.self, forKey: .accountId), + hash: try container.decode(String.self, forKey: .hash), + namespace: try container.decode(Int.self, forKey: .namespace), + dataLength: try container.decode(Int.self, forKey: .dataLength), + dataTooLong: ((try? container.decode(Bool.self, forKey: .dataTooLong)) ?? false) + ) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushNotificationAPIRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushNotificationAPIRequest.swift new file mode 100644 index 000000000..671b0b7a5 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushNotificationAPIRequest.swift @@ -0,0 +1,33 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +public struct PushNotificationAPIRequest: Encodable { + private enum CodingKeys: String, CodingKey { + case method + case body = "params" + } + + internal let endpoint: PushNotificationAPI.Endpoint + internal let body: T + + // MARK: - Initialization + + public init( + endpoint: PushNotificationAPI.Endpoint, + body: T + ) { + self.endpoint = endpoint + self.body = body + } + + // MARK: - Codable + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(endpoint.rawValue, forKey: .method) + try container.encode(body, forKey: .body) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift new file mode 100644 index 000000000..9417d232d --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift @@ -0,0 +1,153 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionSnodeKit + +extension PushNotificationAPI { + struct SubscribeRequest: Encodable { + struct ServiceInfo: Codable { + private enum CodingKeys: String, CodingKey { + case token + } + + private let token: String + + // MARK: - Initialization + + init(token: String) { + self.token = token + } + } + + private enum CodingKeys: String, CodingKey { + case pubkey + case ed25519PublicKey = "session_ed25519" + case subkey = "subkey_tag" + case namespaces + case includeMessageData = "data" + case timestamp = "sig_ts" + case signatureBase64 = "signature" + case service + case serviceInfo = "service_info" + case notificationsEncryptionKey = "enc_key" + } + + /// The 33-byte account being subscribed to; typically a session ID. + private let pubkey: String + + /// List of integer namespace (-32768 through 32767). These must be sorted in ascending order. + private let namespaces: [SnodeAPI.Namespace] + + /// If provided and true then notifications will include the body of the message (as long as it isn't too large); if false then the body will + /// not be included in notifications. + private let includeMessageData: Bool + + /// Dict of service-specific data; typically this includes just a "token" field with a device-specific token, but different services in the + /// future may have different input requirements. + private let serviceInfo: ServiceInfo + + /// 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 using this key. Though + /// it is permitted for this to change, it is recommended that the device generate this once and persist it. + private let notificationsEncryptionKey: Data + + /// 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth + private let subkey: String? + + /// The signature unix timestamp (seconds, not ms) + private let timestamp: Int64 + + /// When the pubkey value starts with 05 (i.e. a session ID) this is the underlying ed25519 32-byte pubkey associated with the session + /// ID. When not 05, this field should not be provided. + private let ed25519PublicKey: [UInt8] + + /// Secret key used to generate the signature (**Not** sent with the request) + private let ed25519SecretKey: [UInt8] + + // MARK: - Initialization + + init( + pubkey: String, + namespaces: [SnodeAPI.Namespace], + includeMessageData: Bool, + serviceInfo: ServiceInfo, + notificationsEncryptionKey: Data, + subkey: String?, + timestamp: TimeInterval, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8] + ) { + self.pubkey = pubkey + self.namespaces = namespaces + self.includeMessageData = includeMessageData + self.serviceInfo = serviceInfo + self.notificationsEncryptionKey = notificationsEncryptionKey + self.subkey = subkey + self.timestamp = Int64(timestamp) // Server expects rounded seconds + self.ed25519PublicKey = ed25519PublicKey + self.ed25519SecretKey = ed25519SecretKey + } + + // MARK: - Coding + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + // Generate the signature for the request for encoding + let signatureBase64: String = try generateSignature().toBase64() + try container.encode(pubkey, forKey: .pubkey) + try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey) + try container.encodeIfPresent(subkey, forKey: .subkey) + try container.encode(namespaces.map { $0.rawValue}.sorted(), forKey: .namespaces) + try container.encode(includeMessageData, forKey: .includeMessageData) + try container.encode(timestamp, forKey: .timestamp) + try container.encode(signatureBase64, forKey: .signatureBase64) + try container.encode(Service.apns, forKey: .service) + try container.encode(serviceInfo, forKey: .serviceInfo) + try container.encode(notificationsEncryptionKey.toHexString(), forKey: .notificationsEncryptionKey) + } + + // MARK: - Abstract Methods + + func generateSignature() throws -> [UInt8] { + /// The signature data collected and stored here is used by the PN server to subscribe to the swarms + /// for the given account; the specific rules are governed by the storage server, but in general: + /// + /// A signature must have been produced (via the timestamp) within the past 14 days. It is + /// recommended that clients generate a new signature whenever they re-subscribe, and that + /// re-subscriptions happen more frequently than once every 14 days. + /// + /// A signature is signed using the account's Ed25519 private key (or Ed25519 subkey, if using + /// subkey authentication with a `subkey_tag`, for future closed group subscriptions), and signs the value: + /// `"MONITOR" || HEX(ACCOUNT) || SIG_TS || DATA01 || NS[0] || "," || ... || "," || NS[n]` + /// + /// Where `SIG_TS` is the `sig_ts` value as a base-10 string; `DATA01` is either "0" or "1" depending + /// on whether the subscription wants message data included; and the trailing `NS[i]` values are a + /// comma-delimited list of namespaces that should be subscribed to, in the same sorted order as + /// the `namespaces` parameter. + let verificationBytes: [UInt8] = "MONITOR".bytes + .appending(contentsOf: pubkey.bytes) + .appending(contentsOf: "\(timestamp)".bytes) + .appending(contentsOf: (includeMessageData ? "1" : "0").bytes) + .appending( + contentsOf: namespaces + .map { $0.rawValue } // Intentionally not using `verificationString` here + .sorted() + .map { "\($0)" } + .joined(separator: ",") + .bytes + ) + + // TODO: Need to add handling for subkey auth + guard + let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( + message: verificationBytes, + secretKey: ed25519SecretKey + ) + else { + throw SnodeAPIError.signingFailed + } + + return signatureBytes + } + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift new file mode 100644 index 000000000..c2298e1c2 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift @@ -0,0 +1,31 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct SubscribeResponse: Codable { + /// Flag indicating the success of the registration + let success: Bool? + + /// Value is `true` upon an initial registration + let added: Bool? + + /// Value is `true` upon a renewal/update registration + let updated: Bool? + + /// This will be one of the errors found here: + /// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21 + /// + /// Values at the time of writing are: + /// OK = 0 // Great Success! + /// BAD_INPUT = 1 // Unparseable, invalid values, missing required arguments, etc. (details in the string) + /// SERVICE_NOT_AVAILABLE = 2 // The requested service name isn't currently available + /// SERVICE_TIMEOUT = 3 // The backend service did not response + /// ERROR = 4 // There was some other error processing the subscription (details in the string) + /// INTERNAL_ERROR = 5 // An internal program error occured processing the request + let error: Int? + + /// Includes additional information about the error + let message: String? + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift new file mode 100644 index 000000000..3d76f76ab --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift @@ -0,0 +1,111 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionSnodeKit + +extension PushNotificationAPI { + struct UnsubscribeRequest: Encodable { + struct ServiceInfo: Codable { + private enum CodingKeys: String, CodingKey { + case token + } + + private let token: String + + // MARK: - Initialization + + init(token: String) { + self.token = token + } + } + + private enum CodingKeys: String, CodingKey { + case pubkey + case ed25519PublicKey = "session_ed25519" + case subkey = "subkey_tag" + case timestamp = "sig_ts" + case signatureBase64 = "signature" + case service + case serviceInfo = "service_info" + } + + /// The 33-byte account being subscribed to; typically a session ID. + private let pubkey: String + + /// Dict of service-specific data; typically this includes just a "token" field with a device-specific token, but different services in the + /// future may have different input requirements. + private let serviceInfo: ServiceInfo + + /// 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth + private let subkey: String? + + /// The signature unix timestamp (seconds, not ms) + private let timestamp: Int64 + + /// When the pubkey value starts with 05 (i.e. a session ID) this is the underlying ed25519 32-byte pubkey associated with the session + /// ID. When not 05, this field should not be provided. + private let ed25519PublicKey: [UInt8] + + /// Secret key used to generate the signature (**Not** sent with the request) + private let ed25519SecretKey: [UInt8] + + // MARK: - Initialization + + init( + pubkey: String, + serviceInfo: ServiceInfo, + subkey: String?, + timestamp: TimeInterval, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8] + ) { + self.pubkey = pubkey + self.serviceInfo = serviceInfo + self.subkey = subkey + self.timestamp = Int64(timestamp) // Server expects rounded seconds + self.ed25519PublicKey = ed25519PublicKey + self.ed25519SecretKey = ed25519SecretKey + } + + // MARK: - Coding + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + // Generate the signature for the request for encoding + let signatureBase64: String = try generateSignature().toBase64() + try container.encode(pubkey, forKey: .pubkey) + try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey) + try container.encodeIfPresent(subkey, forKey: .subkey) + try container.encode(timestamp, forKey: .timestamp) + try container.encode(signatureBase64, forKey: .signatureBase64) + try container.encode(Service.apns, forKey: .service) + try container.encode(serviceInfo, forKey: .serviceInfo) + } + + // MARK: - Abstract Methods + + func generateSignature() throws -> [UInt8] { + /// A signature is signed using the account's Ed25519 private key (or Ed25519 subkey, if using + /// subkey authentication with a `subkey_tag`, for future closed group subscriptions), and signs the value: + /// `"UNSUBSCRIBE" || HEX(ACCOUNT) || SIG_TS` + /// + /// Where `SIG_TS` is the `sig_ts` value as a base-10 string and must be within 24 hours of the current time. + let verificationBytes: [UInt8] = "UNSUBSCRIBE".bytes + .appending(contentsOf: pubkey.bytes) + .appending(contentsOf: "\(timestamp)".data(using: .ascii)?.bytes) + + // TODO: Need to add handling for subkey auth + guard + let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( + message: verificationBytes, + secretKey: ed25519SecretKey + ) + else { + throw SnodeAPIError.signingFailed + } + + return signatureBytes + } + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift new file mode 100644 index 000000000..03b38c524 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift @@ -0,0 +1,31 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct UnsubscribeResponse: Codable { + /// Flag indicating the success of the registration + let success: Bool? + + /// Value is `true` upon an initial registration + let added: Bool? + + /// Value is `true` upon a renewal/update registration + let updated: Bool? + + /// This will be one of the errors found here: + /// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21 + /// + /// Values at the time of writing are: + /// OK = 0 // Great Success! + /// BAD_INPUT = 1 // Unparseable, invalid values, missing required arguments, etc. (details in the string) + /// SERVICE_NOT_AVAILABLE = 2 // The requested service name isn't currently available + /// SERVICE_TIMEOUT = 3 // The backend service did not response + /// ERROR = 4 // There was some other error processing the subscription (details in the string) + /// INTERNAL_ERROR = 5 // An internal program error occured processing the request + let error: Int? + + /// Includes additional information about the error + let message: String? + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 78886b8c8..6e32886ac 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -3,136 +3,33 @@ import Foundation import Combine import GRDB +import Sodium import SessionSnodeKit import SessionUtilitiesKit public enum PushNotificationAPI { - struct RegistrationRequestBody: Codable { - let token: String - let pubKey: String? - } - - struct NotifyRequestBody: Codable { - enum CodingKeys: String, CodingKey { - case data - case sendTo = "send_to" - } - - let data: String - let sendTo: String - } - - struct ClosedGroupRequestBody: Codable { - let closedGroupPublicKey: String - let pubKey: String - } - - // MARK: - Settings - - public static let server = "https://live.apns.getsession.org" - public static let serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" - + internal static let sodium: Atomic = Atomic(Sodium()) + private static let keychainService: String = "PNKeyChainService" + private static let encryptionKeyKey: String = "PNEncryptionKeyKey" + private static let encryptionKeyLength: Int = 32 private static let maxRetryCount: Int = 4 - private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 - - public enum ClosedGroupOperation: Int { - case subscribe, unsubscribe - - public var endpoint: String { - switch self { - case .subscribe: return "subscribe_closed_group" - case .unsubscribe: return "unsubscribe_closed_group" - } - } - } + private static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60) - // MARK: - Registration + public static let server = "https://push.getsession.org" + public static let serverPublicKey = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" + public static let legacyServer = "https://live.apns.getsession.org" + public static let legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" + + // MARK: - Requests - public static func unregister(_ token: Data) -> AnyPublisher { - let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: token.toHexString(), pubKey: nil) - - guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - // Unsubscribe from all closed groups (including ones the user is no longer a member of, - // just in case) - Storage.shared - .readPublisher { db -> (String, Set) in - ( - getUserHexEncodedPublicKey(db), - try ClosedGroup - .select(.threadId) - .asRequest(of: String.self) - .fetchSet(db) - ) - } - .flatMap { userPublicKey, closedGroupPublicKeys in - Publishers - .MergeMany( - closedGroupPublicKeys - .map { closedGroupPublicKey -> AnyPublisher in - PushNotificationAPI - .performOperation( - .unsubscribe, - for: closedGroupPublicKey, - publicKey: userPublicKey - ) - } - ) - .collect() - .eraseToAnyPublisher() - } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .sinkUntilComplete() - - // Unregister for normal push notifications - let url = URL(string: "\(server)/unregister")! - var request: URLRequest = URLRequest(url: url) - request.httpMethod = "POST" - request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] - request.httpBody = body - - return OnionRequestAPI - .sendOnionRequest(request, to: server, with: serverPublicKey) - .map { _, data -> Void in - guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else { - return SNLog("Couldn't unregister from push notifications.") - } - guard response.code != 0 else { - return SNLog("Couldn't unregister from push notifications due to error: \(response.message ?? "nil").") - } - - return () - } - .retry(maxRetryCount) - .handleEvents( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure: SNLog("Couldn't unregister from push notifications.") - } - } - ) - .eraseToAnyPublisher() - } - - public static func register( - with token: Data, - publicKey: String, - isForcedUpdate: Bool + public static func subscribe( + token: Data, + isForcedUpdate: Bool, + using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { let hexEncodedToken: String = token.toHexString() - let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: hexEncodedToken, pubKey: publicKey) - - guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - let oldToken: String? = UserDefaults.standard[.deviceToken] - let lastUploadTime: Double = UserDefaults.standard[.lastDeviceTokenUpload] + let oldToken: String? = dependencies.standardUserDefaults[.deviceToken] + let lastUploadTime: Double = dependencies.standardUserDefaults[.lastDeviceTokenUpload] let now: TimeInterval = Date().timeIntervalSince1970 guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else { @@ -142,153 +39,470 @@ public enum PushNotificationAPI { .eraseToAnyPublisher() } - let url = URL(string: "\(server)/register")! - var request: URLRequest = URLRequest(url: url) - request.httpMethod = "POST" - request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] - request.httpBody = body - - return Publishers - .MergeMany( - [ - OnionRequestAPI - .sendOnionRequest(request, to: server, with: serverPublicKey) - .map { _, data -> Void in - guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else { - return SNLog("Couldn't register device token.") - } - guard response.code != 0 else { - return SNLog("Couldn't register device token due to error: \(response.message ?? "nil").") - } - - UserDefaults.standard[.deviceToken] = hexEncodedToken - UserDefaults.standard[.lastDeviceTokenUpload] = now - UserDefaults.standard[.isUsingFullAPNs] = true - return () - } - .retry(maxRetryCount) - .handleEvents( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure: SNLog("Couldn't register device token.") - } - } - ) - .eraseToAnyPublisher() - ].appending( - contentsOf: Storage.shared - .read { db -> [String] in - try ClosedGroup - .select(.threadId) - .joining( - required: ClosedGroup.members - .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) - ) - .asRequest(of: String.self) - .fetchAll(db) - } - .defaulting(to: []) - .map { closedGroupPublicKey -> AnyPublisher in - PushNotificationAPI - .performOperation( - .subscribe, - for: closedGroupPublicKey, - publicKey: publicKey - ) - } - ) - ) - .collect() - .map { _ in () } - .eraseToAnyPublisher() - } - - public static func performOperation( - _ operation: ClosedGroupOperation, - for closedGroupPublicKey: String, - publicKey: String - ) -> AnyPublisher { - let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs] - let requestBody: ClosedGroupRequestBody = ClosedGroupRequestBody( - closedGroupPublicKey: closedGroupPublicKey, - pubKey: publicKey - ) - - guard isUsingFullAPNs else { + guard let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies) else { + SNLog("Unable to retrieve PN encryption key.") return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() } - guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - let url = URL(string: "\(server)/\(operation.endpoint)")! - var request: URLRequest = URLRequest(url: url) - request.httpMethod = "POST" - request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] - request.httpBody = body - - return OnionRequestAPI - .sendOnionRequest(request, to: server, with: serverPublicKey) - .map { _, data in - guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else { - return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).") - } - guard response.code != 0 else { - return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(response.message ?? "nil").") + // TODO: Need to generate requests for each updated group as well + return dependencies.storage + .readPublisher(using: dependencies) { db -> (SubscribeRequest, String, Set) in + guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { + throw SnodeAPIError.noKeyPair } - return () + let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) + let request: SubscribeRequest = SubscribeRequest( + pubkey: currentUserPublicKey, + namespaces: [.default], + // Note: Unfortunately we always need the message content because without the content + // control messages can't be distinguished from visible messages which results in the + // 'generic' notification being shown when receiving things like typing indicator updates + includeMessageData: true, + serviceInfo: SubscribeRequest.ServiceInfo( + token: hexEncodedToken + ), + notificationsEncryptionKey: notificationsEncryptionKey, + subkey: nil, + timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey + ) + + return ( + request, + currentUserPublicKey, + try ClosedGroup + .select(.threadId) + .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == currentUserPublicKey) + ) + .asRequest(of: String.self) + .fetchSet(db) + ) } - .retry(maxRetryCount) + .flatMap { request, currentUserPublicKey, legacyGroupIds -> AnyPublisher in + Publishers + .MergeMany( + [ + PushNotificationAPI + .send( + request: PushNotificationAPIRequest( + endpoint: .subscribe, + body: request + ), + using: dependencies + ) + .decoded(as: SubscribeResponse.self, using: dependencies) + .retry(maxRetryCount, using: dependencies) + .handleEvents( + receiveOutput: { _, response in + guard response.success == true else { + return SNLog("Couldn't subscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").") + } + + dependencies.standardUserDefaults[.deviceToken] = hexEncodedToken + dependencies.standardUserDefaults[.lastDeviceTokenUpload] = now + dependencies.standardUserDefaults[.isUsingFullAPNs] = true + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure: SNLog("Couldn't subscribe for push notifications.") + } + } + ) + .map { _ in () } + .eraseToAnyPublisher(), + // FIXME: Remove this once legacy groups are deprecated + PushNotificationAPI.subscribeToLegacyGroups( + forced: true, + token: hexEncodedToken, + currentUserPublicKey: currentUserPublicKey, + legacyGroupIds: legacyGroupIds, + using: dependencies + ) + ] + ) + .collect() + .map { _ in () } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + public static func unsubscribe( + token: Data, + using dependencies: Dependencies = Dependencies() + ) -> AnyPublisher { + let hexEncodedToken: String = token.toHexString() + + // FIXME: Remove this once legacy groups are deprecated + /// Unsubscribe from all legacy groups (including ones the user is no longer a member of, just in case) + dependencies.storage + .readPublisher(using: dependencies) { db -> (String, Set) in + ( + getUserHexEncodedPublicKey(db, using: dependencies), + try ClosedGroup + .select(.threadId) + .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .asRequest(of: String.self) + .fetchSet(db) + ) + } + .flatMap { currentUserPublicKey, legacyGroupIds in + Publishers + .MergeMany( + legacyGroupIds + .map { legacyGroupId -> AnyPublisher in + PushNotificationAPI + .unsubscribeFromLegacyGroup( + legacyGroupId: legacyGroupId, + currentUserPublicKey: currentUserPublicKey, + using: dependencies + ) + } + ) + .collect() + .eraseToAnyPublisher() + } + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .sinkUntilComplete() + + // TODO: Need to generate requests for each updated group as well + return dependencies.storage + .readPublisher(using: dependencies) { db -> UnsubscribeRequest in + guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { + throw SnodeAPIError.noKeyPair + } + + return UnsubscribeRequest( + pubkey: getUserHexEncodedPublicKey(db, using: dependencies), + serviceInfo: UnsubscribeRequest.ServiceInfo( + token: hexEncodedToken + ), + subkey: nil, + timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey + ) + } + .flatMap { request -> AnyPublisher in + PushNotificationAPI + .send( + request: PushNotificationAPIRequest( + endpoint: .unsubscribe, + body: request + ), + using: dependencies + ) + .decoded(as: UnsubscribeResponse.self, using: dependencies) + .retry(maxRetryCount, using: dependencies) + .handleEvents( + receiveOutput: { _, response in + guard response.success == true else { + return SNLog("Couldn't unsubscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").") + } + + dependencies.standardUserDefaults[.deviceToken] = nil + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure: SNLog("Couldn't unsubscribe for push notifications.") + } + } + ) + .map { _ in () } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + // MARK: - Legacy Notifications + + // FIXME: Remove this once legacy notifications and legacy groups are deprecated + public static func legacyNotify( + recipient: String, + with message: String, + maxRetryCount: Int? = nil, + using dependencies: Dependencies = Dependencies() + ) -> AnyPublisher { + return PushNotificationAPI + .send( + request: PushNotificationAPIRequest( + endpoint: .legacyNotify, + body: LegacyNotifyRequest( + data: message, + sendTo: recipient + ) + ), + using: dependencies + ) + .decoded(as: LegacyPushServerResponse.self, using: dependencies) + .retry(maxRetryCount ?? PushNotificationAPI.maxRetryCount, using: dependencies) .handleEvents( + receiveOutput: { _, response in + guard response.code != 0 else { + return SNLog("Couldn't send push notification due to error: \(response.message ?? "nil").") + } + }, receiveCompletion: { result in switch result { case .finished: break - case .failure: - SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).") + case .failure: SNLog("Couldn't send push notification.") } } ) + .map { _ in () } .eraseToAnyPublisher() } - // MARK: - Notify + // MARK: - Legacy Groups - public static func notify( - recipient: String, - with message: String, - maxRetryCount: Int? = nil + // FIXME: Remove this once legacy groups are deprecated + public static func subscribeToLegacyGroups( + forced: Bool = false, + token: String? = nil, + currentUserPublicKey: String, + legacyGroupIds: Set, + using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { - let requestBody: NotifyRequestBody = NotifyRequestBody(data: message, sendTo: recipient) + let isUsingFullAPNs = dependencies.standardUserDefaults[.isUsingFullAPNs] - guard let body: Data = try? JSONEncoder().encode(requestBody) else { + // Only continue if PNs are enabled and we have a device token + guard + (forced || isUsingFullAPNs), + let deviceToken: String = (token ?? dependencies.standardUserDefaults[.deviceToken]) + else { + return Just(()) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + + return PushNotificationAPI + .send( + request: PushNotificationAPIRequest( + endpoint: .legacyGroupsOnlySubscribe, + body: LegacyGroupOnlyRequest( + token: deviceToken, + pubKey: currentUserPublicKey, + device: "ios", + legacyGroupPublicKeys: legacyGroupIds + ) + ), + using: dependencies + ) + .decoded(as: LegacyPushServerResponse.self, using: dependencies) + .retry(maxRetryCount, using: dependencies) + .handleEvents( + receiveOutput: { _, response in + guard response.code != 0 else { + return SNLog("Couldn't subscribe for legacy groups due to error: \(response.message ?? "nil").") + } + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure: SNLog("Couldn't subscribe for legacy groups.") + } + } + ) + .map { _ in () } + .eraseToAnyPublisher() + } + + // FIXME: Remove this once legacy groups are deprecated + public static func unsubscribeFromLegacyGroup( + legacyGroupId: String, + currentUserPublicKey: String, + using dependencies: Dependencies = Dependencies() + ) -> AnyPublisher { + return PushNotificationAPI + .send( + request: PushNotificationAPIRequest( + endpoint: .legacyGroupUnsubscribe, + body: LegacyGroupRequest( + pubKey: currentUserPublicKey, + closedGroupPublicKey: legacyGroupId + ) + ), + using: dependencies + ) + .decoded(as: LegacyPushServerResponse.self, using: dependencies) + .retry(maxRetryCount, using: dependencies) + .handleEvents( + receiveOutput: { _, response in + guard response.code != 0 else { + return SNLog("Couldn't unsubscribe for legacy group: \(legacyGroupId) due to error: \(response.message ?? "nil").") + } + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure: SNLog("Couldn't unsubscribe for legacy group: \(legacyGroupId).") + } + } + ) + .map { _ in () } + .eraseToAnyPublisher() + } + + // MARK: - Notification Handling + + public static func processNotification( + notificationContent: UNNotificationContent, + dependencies: Dependencies = Dependencies() + ) -> (envelope: SNProtoEnvelope?, result: ProcessResult) { + // Make sure the notification is from the updated push server + guard notificationContent.userInfo["spns"] != nil else { + guard + let base64EncodedData: String = notificationContent.userInfo["ENCRYPTED_DATA"] as? String, + let data: Data = Data(base64Encoded: base64EncodedData), + let envelope: SNProtoEnvelope = try? MessageWrapper.unwrap(data: data) + else { return (nil, .legacyFailure) } + + // We only support legacy notifications for legacy group conversations + guard envelope.type == .closedGroupMessage else { return (envelope, .legacyForceSilent) } + + return (envelope, .legacySuccess) + } + + guard + let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String, + let encData: Data = Data(base64Encoded: base64EncodedEncString), + let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies), + encData.count > dependencies.crypto.size(.aeadXChaCha20NonceBytes) + else { return (nil, .failure) } + + let nonce: Data = encData[0.. = try? Bencode.decodeResponse(from: decryptedData) else { + return (nil, .failure) + } + + // If the metadata says that the message was too large then we should show the generic + // notification (this is a valid case) + guard !notification.info.dataTooLong else { return (nil, .success) } + + // Check that the body we were given is valid + guard + let notificationData: Data = notification.data, + notification.info.dataLength == notificationData.count, + let envelope = try? MessageWrapper.unwrap(data: notificationData) + else { return (nil, .failure) } + + // Success, we have the notification content + return (envelope, .success) + } + + // MARK: - Security + + @discardableResult private static func getOrGenerateEncryptionKey(using dependencies: Dependencies) throws -> Data { + do { + var encryptionKey: Data = try SSKDefaultKeychainStorage.shared.data( + forService: keychainService, + key: encryptionKeyKey + ) + defer { encryptionKey.resetBytes(in: 0..( + request: PushNotificationAPIRequest, + using dependencies: Dependencies + ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { + guard + let url: URL = URL(string: "\(request.endpoint.server)/\(request.endpoint.rawValue)"), + let payload: Data = try? JSONEncoder().encode(request.body) + else { return Fail(error: HTTPError.invalidJSON) .eraseToAnyPublisher() } - let url = URL(string: "\(server)/notify")! - var request: URLRequest = URLRequest(url: url) - request.httpMethod = "POST" - request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] - request.httpBody = body + guard Features.useOnionRequests else { + return HTTP + .execute( + .post, + "\(request.endpoint.server)/\(request.endpoint.rawValue)", + body: payload + ) + .map { response in (HTTP.ResponseInfo(code: -1, headers: [:]), response) } + .eraseToAnyPublisher() + } - return OnionRequestAPI - .sendOnionRequest(request, to: server, with: serverPublicKey) - .map { _, data -> Void in - guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else { - return SNLog("Couldn't send push notification.") - } - guard response.code != 0 else { - return SNLog("Couldn't send push notification due to error: \(response.message ?? "nil").") - } - - return () - } - .retry(maxRetryCount ?? PushNotificationAPI.maxRetryCount) + var urlRequest: URLRequest = URLRequest(url: url) + urlRequest.httpMethod = "POST" + urlRequest.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] + urlRequest.httpBody = payload + + return dependencies.network + .send( + .onionRequest( + urlRequest, + to: request.endpoint.server, + with: request.endpoint.serverPublicKey + ) + ) .eraseToAnyPublisher() } } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift new file mode 100644 index 000000000..1c72b1629 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension PushNotificationAPI { + enum ProcessResult { + case success + case failure + case legacySuccess + case legacyFailure + case legacyForceSilent + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift new file mode 100644 index 000000000..072abcc60 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift @@ -0,0 +1,41 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension PushNotificationAPI { + enum Endpoint: String { + case subscribe = "subscribe" + case unsubscribe = "unsubscribe" + + // MARK: - Legacy Endpoints + + case legacyNotify = "notify" + case legacyRegister = "register" + case legacyUnregister = "unregister" + case legacyGroupsOnlySubscribe = "register_legacy_groups_only" + case legacyGroupSubscribe = "subscribe_closed_group" + case legacyGroupUnsubscribe = "unsubscribe_closed_group" + + // MARK: - Convenience + + var server: String { + switch self { + case .legacyNotify, .legacyRegister, .legacyUnregister, + .legacyGroupsOnlySubscribe, .legacyGroupSubscribe, .legacyGroupUnsubscribe: + return PushNotificationAPI.legacyServer + + default: return PushNotificationAPI.server + } + } + + var serverPublicKey: String { + switch self { + case .legacyNotify, .legacyRegister, .legacyUnregister, + .legacyGroupsOnlySubscribe, .legacyGroupSubscribe, .legacyGroupUnsubscribe: + return PushNotificationAPI.legacyServerPublicKey + + default: return PushNotificationAPI.serverPublicKey + } + } + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift new file mode 100644 index 000000000..b9aeb904b --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift @@ -0,0 +1,10 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + enum Service: String, Codable { + case apns + case sandbox = "apns-sandbox" // Use for push notifications in Testnet + } +} diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift index 019b19829..b6a8b86f0 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift @@ -573,7 +573,8 @@ private extension SessionUtil { count: ProfileManager.avatarAES256KeyByteLength ) ), - lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000) + lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000), + lastBlocksCommunityMessageRequests: 0 ) result[contactId] = ContactData( diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift index 25b2606e6..4a9c8d286 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift @@ -210,6 +210,46 @@ internal extension SessionUtil { return updated } + static func hasSetting(_ db: Database, forKey key: String) throws -> Bool { + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + // Currently the only synced setting is 'checkForCommunityMessageRequests' + switch key { + case Setting.BoolKey.checkForCommunityMessageRequests.rawValue: + return try SessionUtil + .config(for: .userProfile, publicKey: userPublicKey) + .wrappedValue + .map { conf -> Bool in (try SessionUtil.rawBlindedMessageRequestValue(in: conf) >= 0) } + .defaulting(to: false) + + default: return false + } + } + + static func updatingSetting(_ db: Database, _ updated: Setting?) throws { + // Don't current support any nullable settings + guard let updatedSetting: Setting = updated else { return } + + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + // Currently the only synced setting is 'checkForCommunityMessageRequests' + switch updatedSetting.id { + case Setting.BoolKey.checkForCommunityMessageRequests.rawValue: + try SessionUtil.performAndPushChange( + db, + for: .userProfile, + publicKey: userPublicKey + ) { conf in + try SessionUtil.updateSettings( + checkForCommunityMessageRequests: updatedSetting.unsafeValue(as: Bool.self), + in: conf + ) + } + + default: break + } + } + static func kickFromConversationUIIfNeeded(removedThreadIds: [String]) { guard !removedThreadIds.isEmpty else { return } diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift index ed522930e..4416eee82 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift @@ -12,6 +12,10 @@ internal extension SessionUtil { Profile.Columns.profileEncryptionKey ] + static let syncedSettings: [String] = [ + Setting.BoolKey.checkForCommunityMessageRequests.rawValue + ] + // MARK: - Incoming Changes static func handleUserProfileUpdate( @@ -115,6 +119,17 @@ internal extension SessionUtil { } } + // Update settings if needed + let updatedAllowBlindedMessageRequests: Int32 = user_profile_get_blinded_msgreqs(conf) + let updatedAllowBlindedMessageRequestsBoolValue: Bool = (updatedAllowBlindedMessageRequests >= 1) + + if + updatedAllowBlindedMessageRequests >= 0 && + updatedAllowBlindedMessageRequestsBoolValue != db[.checkForCommunityMessageRequests] + { + db[.checkForCommunityMessageRequests] = updatedAllowBlindedMessageRequestsBoolValue + } + // Create a contact for the current user if needed (also force-approve the current user // in case the account got into a weird state or restored directly from a migration) let userContact: Contact = Contact.fetchOrCreate(db, id: userPublicKey) @@ -159,4 +174,25 @@ internal extension SessionUtil { user_profile_set_nts_priority(conf, priority) } + + static func updateSettings( + checkForCommunityMessageRequests: Bool? = nil, + in conf: UnsafeMutablePointer? + ) throws { + guard conf != nil else { throw SessionUtilError.nilConfigObject } + + if let blindedMessageRequests: Bool = checkForCommunityMessageRequests { + user_profile_set_blinded_msgreqs(conf, (blindedMessageRequests ? 1 : 0)) + } + } +} + +// MARK: - Direct Values + +extension SessionUtil { + static func rawBlindedMessageRequestValue(in conf: UnsafeMutablePointer?) throws -> Int32 { + guard conf != nil else { throw SessionUtilError.nilConfigObject } + + return user_profile_get_blinded_msgreqs(conf) + } } diff --git a/SessionMessagingKit/SessionUtil/Database/Setting+Utilities.swift b/SessionMessagingKit/SessionUtil/Database/Setting+Utilities.swift new file mode 100644 index 000000000..2e178c5b1 --- /dev/null +++ b/SessionMessagingKit/SessionUtil/Database/Setting+Utilities.swift @@ -0,0 +1,58 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +public extension Database { + func setAndUpdateConfig(_ key: Setting.BoolKey, to newValue: Bool) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.DoubleKey, to newValue: Double?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.IntKey, to newValue: Int?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.StringKey, to newValue: String?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.EnumKey, to newValue: T?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.EnumKey, to newValue: T?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + /// Value will be stored as a timestamp in seconds since 1970 + func setAndUpdateConfig(_ key: Setting.DateKey, to newValue: Date?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + private func updateConfigIfNeeded( + _ db: Database, + key: String, + updatedSetting: Setting? + ) throws { + // Before we do anything custom make sure the setting should trigger a change + guard SessionUtil.syncedSettings.contains(key) else { return } + + defer { + // If we changed a column that requires a config update then we may as well automatically + // enqueue a new config sync job once the transaction completes (but only enqueue it once + // per transaction - doing it more than once is pointless) + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in + ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey) + } + } + + try SessionUtil.updatingSetting(db, updatedSetting) + } +} diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 0ee0f5f5a..c9d0c43eb 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -104,7 +104,11 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat public var canWrite: Bool { switch threadVariant { - case .contact: return true + case .contact: + guard threadIsMessageRequest == true else { return true } + + return (profile?.blocksCommunityMessageRequests != true) + case .legacyGroup, .group: return ( currentUserIsClosedGroupMember == true && diff --git a/SessionMessagingKit/Utilities/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/Crypto+SessionMessagingKit.swift index d2a974ef5..fa285dd36 100644 --- a/SessionMessagingKit/Utilities/Crypto+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/Crypto+SessionMessagingKit.swift @@ -110,6 +110,14 @@ public extension Crypto.Action { } } +// MARK: - AeadXChaCha20Poly1305Ietf + +public extension Crypto.Size { + static let aeadXChaCha20NonceBytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20NonceBytes") { + Sodium().aead.xchacha20poly1305ietf.NonceBytes + } +} + // MARK: - Ed25519 public extension Crypto.Action { diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index 34a00860e..4713e8ce1 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -65,6 +65,9 @@ public extension Setting.BoolKey { /// Controls whether concurrent audio messages should automatically be played after the one the user starts /// playing finishes static let shouldAutoPlayConsecutiveAudioMessages: Setting.BoolKey = "shouldAutoPlayConsecutiveAudioMessages" + + /// Controls whether the device will poll for community message requests (SOGS `/inbox` endpoint) + static let checkForCommunityMessageRequests: Setting.BoolKey = "checkForCommunityMessageRequests" } public extension Setting.StringKey { diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index e8407c8c7..1a1488984 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -498,6 +498,7 @@ public struct ProfileManager { _ db: Database, publicKey: String, name: String?, + blocksCommunityMessageRequests: Bool? = nil, avatarUpdate: AvatarUpdate, sentTimestamp: TimeInterval, calledFromConfigHandling: Bool = false, @@ -515,6 +516,12 @@ public struct ProfileManager { } } + // Blocks community message requets flag + if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > profile.lastBlocksCommunityMessageRequests { + profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) + profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) + } + // Profile picture & profile key var avatarNeedsDownload: Bool = false var targetAvatarUrl: String? = nil diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift index ce0ba7a6e..849cdc85b 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift @@ -285,6 +285,17 @@ class ConfigUserProfileSpec { ) user_profile_set_pic(conf2, p2) + user_profile_set_nts_expiry(conf2, 86400) + expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) + + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) + user_profile_set_blinded_msgreqs(conf2, 0) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(0)) + user_profile_set_blinded_msgreqs(conf2, -1) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) + user_profile_set_blinded_msgreqs(conf2, 1) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) + // Both have changes, so push need a push expect(config_needs_push(conf)).to(beTrue()) expect(config_needs_push(conf2)).to(beTrue()) @@ -364,6 +375,10 @@ class ConfigUserProfileSpec { .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) expect(user_profile_get_nts_priority(conf)).to(equal(9)) expect(user_profile_get_nts_priority(conf2)).to(equal(9)) + expect(user_profile_get_nts_expiry(conf)).to(equal(86400)) + expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) + expect(user_profile_get_blinded_msgreqs(conf)).to(equal(1)) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) let fakeHash4: String = "fakehash4" var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 8f815c2eb..8586c99d1 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -243,10 +243,12 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- when blinded - context("when blinded") { + // MARK: -- when blinded and checking for message requests + context("when blinded and checking for message requests") { beforeEach { mockStorage.write { db in + db[.checkForCommunityMessageRequests] = true + _ = try Capability.deleteAll(db) try Capability(openGroupServer: "testserver", variant: .sogs, isMissing: false).insert(db) try Capability(openGroupServer: "testserver", variant: .blind, isMissing: false).insert(db) @@ -339,6 +341,69 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints).to(contain(.outboxSince(id: 125))) } } + + // MARK: -- when blinded and not checking for message requests + context("when blinded and not checking for message requests") { + beforeEach { + mockStorage.write { db in + db[.checkForCommunityMessageRequests] = false + + _ = try Capability.deleteAll(db) + try Capability(openGroupServer: "testserver", variant: .sogs, isMissing: false).insert(db) + try Capability(openGroupServer: "testserver", variant: .blind, isMissing: false).insert(db) + } + } + + // MARK: ---- includes the inbox and outbox endpoints + it("does not include the inbox endpoint") { + let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in + try OpenGroupAPI.preparedPoll( + db, + server: "testserver", + hasPerformedInitialPoll: false, + timeSinceLastPoll: 0, + using: dependencies + ) + } + + expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox)) + } + + // MARK: ---- does not retrieve recent inbox messages if there was no last message + it("does not retrieve recent inbox messages if there was no last message") { + let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in + try OpenGroupAPI.preparedPoll( + db, + server: "testserver", + hasPerformedInitialPoll: true, + timeSinceLastPoll: 0, + using: dependencies + ) + } + + expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox)) + } + + // MARK: ---- does not retrieve inbox messages since the last message if there was one + it("does not retrieve inbox messages since the last message if there was one") { + mockStorage.write { db in + try OpenGroup + .updateAll(db, OpenGroup.Columns.inboxLatestMessageId.set(to: 124)) + } + + let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in + try OpenGroupAPI.preparedPoll( + db, + server: "testserver", + hasPerformedInitialPoll: true, + timeSinceLastPoll: 0, + using: dependencies + ) + } + + expect(preparedRequest?.batchEndpoints).toNot(contain(.inboxSince(id: 124))) + } + } } // MARK: - when preparing a capabilities request diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index acff494bb..f091f4eb6 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -5,6 +5,7 @@ import GRDB import UserNotifications import SignalUtilitiesKit import SessionMessagingKit +import SessionUtilitiesKit public class NSENotificationPresenter: NSObject, NotificationsProtocol { private var notifications: [String: UNNotificationRequest] = [:] diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 6237975be..22c6f4948 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -9,6 +9,7 @@ import BackgroundTasks import SessionMessagingKit import SignalUtilitiesKit import SignalCoreKit +import SessionUtilitiesKit public final class NotificationServiceExtension: UNNotificationServiceExtension { private var didPerformSetup = false @@ -25,8 +26,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension self.contentHandler = contentHandler self.request = request - Storage.resumeDatabaseAccess() - guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { return self.completeSilenty() } @@ -36,10 +35,16 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension return self.completeSilenty() } + /// Create the context if we don't have it (needed before _any_ interaction with the database) + if !HasAppContext() { + SetCurrentAppContext(NotificationServiceExtensionContext()) + } + let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing]) .defaulting(to: false) // Perform main setup + Storage.resumeDatabaseAccess() DispatchQueue.main.sync { self.setUpIfNecessary() { } } // Handle the push notification @@ -57,12 +62,22 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) } + let (maybeEnvelope, result) = PushNotificationAPI.processNotification( + notificationContent: notificationContent + ) + guard - let base64EncodedData: String = notificationContent.userInfo["ENCRYPTED_DATA"] as? String, - let data: Data = Data(base64Encoded: base64EncodedData), - let envelope = try? MessageWrapper.unwrap(data: data) + (result == .success || result == .legacySuccess), + let envelope: SNProtoEnvelope = maybeEnvelope else { - return self.handleFailure(for: notificationContent) + switch result { + // If we got an explicit failure, or we got a success but no content then show + // the fallback notification + case .success, .legacySuccess, .failure, .legacyFailure: + return self.handleFailure(for: notificationContent) + + case .legacyForceSilent: return + } } // HACK: It is important to use write synchronously here to avoid a race condition @@ -212,22 +227,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // to process new messages. guard !didPerformSetup else { return } + NSLog("[NotificationServiceExtension] Performing setup") didPerformSetup = true - // This should be the first thing we do. - SetCurrentAppContext(NotificationServiceExtensionContext()) - _ = AppVersion.sharedInstance() Cryptography.seedRandom() - // We should never receive a non-voip notification on an app that doesn't support - // app extensions since we have to inform the service we wanted these, so in theory - // this path should never occur. However, the service does have our push token - // so it is possible that could change in the future. If it does, do nothing - // and don't disturb the user. Messages will be processed when they open the app. - guard Storage.shared[.isReadyForAppExtensions] else { return completeSilenty() } - AppSetup.setupEnvironment( appSpecificBlock: { Environment.shared?.notificationsManager.mutate { @@ -237,8 +243,22 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension migrationsCompletion: { [weak self] result, needsConfigSync in switch result { // Only 'NSLog' works in the extension - viewable via Console.app - case .failure: NSLog("[NotificationServiceExtension] Failed to complete migrations") + case .failure(let error): + NSLog("[NotificationServiceExtension] Failed to complete migrations: \(error)") + self?.completeSilenty() + case .success: + // We should never receive a non-voip notification on an app that doesn't support + // app extensions since we have to inform the service we wanted these, so in theory + // this path should never occur. However, the service does have our push token + // so it is possible that could change in the future. If it does, do nothing + // and don't disturb the user. Messages will be processed when they open the app. + guard Storage.shared[.isReadyForAppExtensions] else { + NSLog("[NotificationServiceExtension] Not ready for extensions") + self?.completeSilenty() + return + } + DispatchQueue.main.async { self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync) } @@ -269,7 +289,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension guard !AppReadiness.isAppReady() else { return } // App isn't ready until storage is ready AND all version migrations are complete. - guard Storage.shared.isValid && migrationsCompleted else { return } + guard Storage.shared.isValid && migrationsCompleted else { + NSLog("[NotificationServiceExtension] Storage invalid") + self.completeSilenty() + return + } SignalUtilitiesKit.Configuration.performMainSetup() @@ -286,8 +310,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } private func completeSilenty() { - SNLog("Complete silenty") - + NSLog("[NotificationServiceExtension] Complete silently") Storage.suspendDatabaseAccess() self.contentHandler!(.init()) diff --git a/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift b/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift index 7c150570b..d642a984c 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift @@ -4,6 +4,7 @@ import Foundation import SignalUtilitiesKit +import SessionUtilitiesKit final class NotificationServiceExtensionContext : NSObject, AppContext { let appLaunchTime = Date() @@ -31,10 +32,6 @@ final class NotificationServiceExtensionContext : NSObject, AppContext { func isInBackground() -> Bool { true } func mainApplicationStateOnLaunch() -> UIApplication.State { .inactive } - func appDatabaseBaseDirectoryPath() -> String { - return appSharedDataDirectoryPath() - } - func appDocumentDirectoryPath() -> String { guard let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else { preconditionFailure("Couldn't get document directory.") @@ -43,14 +40,14 @@ final class NotificationServiceExtensionContext : NSObject, AppContext { } func appSharedDataDirectoryPath() -> String { - guard let groupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: SignalApplicationGroup) else { + guard let groupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: UserDefaults.applicationGroup) else { preconditionFailure("Couldn't get shared data directory.") } return groupContainerURL.path } func appUserDefaults() -> UserDefaults { - guard let userDefaults = UserDefaults(suiteName: SignalApplicationGroup) else { + guard let userDefaults = UserDefaults.sharedLokiProject else { preconditionFailure("Couldn't set up shared user defaults.") } return userDefaults diff --git a/SessionShareExtension/ShareAppExtensionContext.swift b/SessionShareExtension/ShareAppExtensionContext.swift index 4f3417642..4c647fdc7 100644 --- a/SessionShareExtension/ShareAppExtensionContext.swift +++ b/SessionShareExtension/ShareAppExtensionContext.swift @@ -157,7 +157,7 @@ final class ShareAppExtensionContext: NSObject, AppContext { func appSharedDataDirectoryPath() -> String { let targetPath: String? = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: SignalApplicationGroup)? + .containerURL(forSecurityApplicationGroupIdentifier: UserDefaults.applicationGroup)? .path owsAssertDebug(targetPath != nil) @@ -165,10 +165,9 @@ final class ShareAppExtensionContext: NSObject, AppContext { } func appUserDefaults() -> UserDefaults { - let targetUserDefaults: UserDefaults? = UserDefaults(suiteName: SignalApplicationGroup) - owsAssertDebug(targetUserDefaults != nil) + owsAssertDebug(UserDefaults.sharedLokiProject != nil) - return (targetUserDefaults ?? UserDefaults.standard) + return (UserDefaults.sharedLokiProject ?? UserDefaults.standard) } func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 457d5e277..22c0ce612 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -10,6 +10,7 @@ import SignalCoreKit final class ShareNavController: UINavigationController, ShareViewDelegate { public static var attachmentPrepPublisher: AnyPublisher<[SignalAttachment], Error>? + private let versionMigrationsComplete: Atomic = Atomic(false) // MARK: - Error @@ -24,6 +25,8 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { override func loadView() { super.loadView() + + view.themeBackgroundColor = .backgroundPrimary // This should be the first thing we do (Note: If you leave the share context and return to it // the context will already exist, trying to override it results in the share context crashing @@ -78,6 +81,12 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { name: .OWSApplicationDidEnterBackground, object: nil ) + + /// **Note:** If the user opens, dismisses and re-opens the share extension it'll actually use the same instance which + /// results in the `AppSetup` not actually running (and the UI not actually being loaded correctly) - in order to avoid this + /// we call `checkIsAppReady` explicitly here assuming that either the `AppSetup` _hasn't_ complete or won't ever + /// get run + checkIsAppReady(migrationsCompleted: versionMigrationsComplete.wrappedValue) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -100,6 +109,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { } } + versionMigrationsComplete.mutate { $0 = true } checkIsAppReady(migrationsCompleted: true) } @@ -108,9 +118,14 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { // App isn't ready until storage is ready AND all version migrations are complete. guard migrationsCompleted else { return } - guard Storage.shared.isValid else { return } + guard Storage.shared.isValid else { + // If the database is invalid then the UI will handle it + showLockScreenOrMainContent() + return + } guard !AppReadiness.isAppReady() else { // Only mark the app as ready once. + showLockScreenOrMainContent() return } @@ -211,11 +226,11 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { } func shareViewWasCompleted() { - extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } func shareViewWasCancelled() { - extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } func shareViewFailed(error: Error) { diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index c9e7063a0..afc92180a 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -7,6 +7,7 @@ import DifferenceKit import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit +import SessionSnodeKit final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate { private let viewModel: ThreadPickerViewModel = ThreadPickerViewModel() @@ -33,6 +34,18 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView return titleLabel }() + + private lazy var databaseErrorLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.text = "database_inaccessible_error".localized() + result.textAlignment = .center + result.themeTextColor = .textPrimary + result.numberOfLines = 0 + result.isHidden = true + + return result + }() private lazy var tableView: UITableView = { let tableView: UITableView = UITableView() @@ -55,6 +68,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView view.themeBackgroundColor = .backgroundPrimary view.addSubview(tableView) + view.addSubview(databaseErrorLabel) setupLayout() @@ -99,6 +113,10 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView private func setupLayout() { tableView.pin(to: view) + + databaseErrorLabel.pin(.top, to: .top, of: view, withInset: Values.massiveSpacing) + databaseErrorLabel.pin(.leading, to: .leading, of: view, withInset: Values.veryLargeSpacing) + databaseErrorLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.veryLargeSpacing) } // MARK: - Updating @@ -109,7 +127,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView // Start observing for data changes dataChangeObservable = Storage.shared.start( viewModel.observableViewData, - onError: { _ in }, + onError: { [weak self] _ in self?.databaseErrorLabel.isHidden = Storage.shared.isValid }, onChange: { [weak self] viewData in // The defaul scheduler emits changes on the main thread self?.handleUpdates(viewData) diff --git a/SessionSnodeKit/Networking/OnionRequestAPI.swift b/SessionSnodeKit/Networking/OnionRequestAPI.swift index ef06b4a20..4d0e13b3d 100644 --- a/SessionSnodeKit/Networking/OnionRequestAPI.swift +++ b/SessionSnodeKit/Networking/OnionRequestAPI.swift @@ -275,7 +275,7 @@ public enum OnionRequestAPI { if let snode = snode { if let path = paths.first(where: { !$0.contains(snode) }) { buildPaths(reusing: paths, using: dependencies) // Re-build paths in the background - .subscribe(on: DispatchQueue.global(qos: .background)) + .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) .sink(receiveCompletion: { _ in cancellable = [] }, receiveValue: { _ in }) .store(in: &cancellable) @@ -817,50 +817,15 @@ public enum OnionRequestAPI { } public static func process(bencodedData data: Data) -> (info: ResponseInfoType, body: Data?)? { - // The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break - // the data into parts to properly process it - guard let responseString: String = String(data: data, encoding: .ascii), responseString.starts(with: "l") else { + guard let response: BencodeResponse = try? Bencode.decodeResponse(from: data) else { return nil } - let stringParts: [String.SubSequence] = responseString.split(separator: ":") - - guard stringParts.count > 1, let infoLength: Int = Int(stringParts[0].suffix(from: stringParts[0].index(stringParts[0].startIndex, offsetBy: 1))) else { - return nil - } - - let infoStringStartIndex: String.Index = responseString.index(responseString.startIndex, offsetBy: "l\(infoLength):".count) - let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength) - let infoString: String = String(responseString[infoStringStartIndex.. "l\(infoLength)\(infoString)e".count else { - return (responseInfo, nil) - } - - // Extract the response data as well - let dataString: String = String(responseString.suffix(from: infoStringEndIndex)) - let dataStringParts: [String.SubSequence] = dataString.split(separator: ":") - - guard dataStringParts.count > 1, let finalDataLength: Int = Int(dataStringParts[0]), let suffixData: Data = "e".data(using: .utf8) else { - return nil - } - - let dataBytes: Array = Array(data) - let dataEndIndex: Int = (dataBytes.count - suffixData.count) - let dataStartIndex: Int = (dataEndIndex - finalDataLength) - let finalDataBytes: ArraySlice = dataBytes[dataStartIndex..) -> AnyPublisher { + return Publishers + .CombineLatest(refreshTrigger.prepend(()).setFailureType(to: Failure.self), self) + .map { _, value in value } + .eraseToAnyPublisher() + } + + func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> { + scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) } + .compactMap { $0 } + .eraseToAnyPublisher() + } + + func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> { + scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher() + } } // MARK: - Convenience diff --git a/SessionUtilitiesKit/Database/Models/Setting.swift b/SessionUtilitiesKit/Database/Models/Setting.swift index 1f634dac5..163fd01b8 100644 --- a/SessionUtilitiesKit/Database/Models/Setting.swift +++ b/SessionUtilitiesKit/Database/Models/Setting.swift @@ -15,6 +15,7 @@ public struct Setting: Codable, Identifiable, FetchableRecord, PersistableRecord } public var id: String { key } + public var rawValue: Data { value } let key: String let value: Data @@ -53,7 +54,7 @@ extension Setting { self.value = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue)) } - fileprivate func value(as type: Bool.Type) -> Bool? { + public func unsafeValue(as type: Bool.Type) -> Bool? { // Note: The 'assumingMemoryBound' is essentially going to try to convert // the memory into the provided type so can result in invalid data being // returned if the type is incorrect. But it does seem safer than the 'load' @@ -189,7 +190,7 @@ public extension Database { subscript(key: Setting.BoolKey) -> Bool { get { // Default to false if it doesn't exist - (self[key.rawValue]?.value(as: Bool.self) ?? false) + (self[key.rawValue]?.unsafeValue(as: Bool.self) ?? false) } set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) } } @@ -245,4 +246,47 @@ public extension Database { ) } } + + func setting(key: Setting.BoolKey, to newValue: Bool) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.DoubleKey, to newValue: Double?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.IntKey, to newValue: Int?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.StringKey, to newValue: String?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.EnumKey, to newValue: T?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue?.rawValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.EnumKey, to newValue: T?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue?.rawValue) + self[key.rawValue] = result + return result + } + + /// Value will be stored as a timestamp in seconds since 1970 + func setting(key: Setting.DateKey, to newValue: Date?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue.map { $0.timeIntervalSince1970 }) + self[key.rawValue] = result + return result + } } diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 4885e8d02..d298dd343 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -47,8 +47,10 @@ open class Storage { fileprivate var dbWriter: DatabaseWriter? internal var testDbWriter: DatabaseWriter? { dbWriter } + private var unprocessedMigrationRequirements: Atomic<[MigrationRequirement]> = Atomic(MigrationRequirement.allCases) private var migrator: DatabaseMigrator? private var migrationProgressUpdater: Atomic<((String, CGFloat) -> ())>? + private var migrationRequirementProcesser: Atomic<(Database?, MigrationRequirement) -> ()>? // MARK: - Initialization @@ -77,18 +79,24 @@ open class Storage { migrationTargets: (customMigrationTargets ?? []), async: false, onProgressUpdate: nil, + onMigrationRequirement: { _, _ in }, onComplete: { _, _ in } ) return } - // Generate the database KeySpec if needed (this MUST be done before we try to access the database - // as a different thread might attempt to access the database before the key is successfully created) - // - // Note: We reset the bytes immediately after generation to ensure the database key doesn't hang - // around in memory unintentionally - var tmpKeySpec: Data = Storage.getOrGenerateDatabaseKeySpec() - tmpKeySpec.resetBytes(in: 0.. ())?, + onMigrationRequirement: @escaping (Database?, MigrationRequirement) -> (), onComplete: @escaping (Swift.Result, Bool) -> () ) { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { @@ -227,11 +236,22 @@ open class Storage { onProgressUpdate?(totalProgress, totalMinExpectedDuration) } }) + self.migrationRequirementProcesser = Atomic(onMigrationRequirement) // Store the logic to run when the migration completes let migrationCompleted: (Swift.Result) -> () = { [weak self] result in + // Process any unprocessed requirements which need to be processed before completion + // then clear out the state + self?.unprocessedMigrationRequirements.wrappedValue + .filter { $0.shouldProcessAtCompletionIfNotRequired } + .forEach { self?.migrationRequirementProcesser?.wrappedValue(nil, $0) } self?.migrationsCompleted.mutate { $0 = true } self?.migrationProgressUpdater = nil + self?.migrationRequirementProcesser = nil + + // Reset in case there is a requirement on a migration which runs when returning from + // the background + self?.unprocessedMigrationRequirements.mutate { $0 = MigrationRequirement.allCases } // Don't log anything in the case of a 'success' or if the database is suspended (the // latter will happen if the user happens to return to the background too quickly on @@ -282,6 +302,22 @@ open class Storage { } } + public func willStartMigration(_ db: Database, _ migration: Migration.Type) { + let unprocessedRequirements: Set = migration.requirements.asSet() + .intersection(unprocessedMigrationRequirements.wrappedValue.asSet()) + + // No need to do anything if there are no unprocessed requirements + guard !unprocessedRequirements.isEmpty else { return } + + // Process all of the requirements for this migration + unprocessedRequirements.forEach { migrationRequirementProcesser?.wrappedValue(db, $0) } + + // Remove any processed requirements from the list (don't want to process them multiple times) + unprocessedMigrationRequirements.mutate { + $0 = Array($0.asSet().subtracting(migration.requirements.asSet())) + } + } + public static func update( progress: CGFloat, for migration: Migration.Type, @@ -299,7 +335,7 @@ open class Storage { return try SSKDefaultKeychainStorage.shared.data(forService: keychainService, key: dbCipherKeySpecKey) } - @discardableResult private static func getOrGenerateDatabaseKeySpec() -> Data { + @discardableResult private static func getOrGenerateDatabaseKeySpec() throws -> Data { do { var keySpec: Data = try getDatabaseCipherKeySpec() defer { keySpec.resetBytes(in: 0.. Void, onChange: @escaping (Reducer.Value) -> Void ) -> DatabaseCancellable { - guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return AnyDatabaseCancellable(cancel: {}) } + guard isValid, let dbWriter: DatabaseWriter = dbWriter else { + onError(StorageError.databaseInvalid) + return AnyDatabaseCancellable(cancel: {}) + } return observation.start( in: dbWriter, diff --git a/SessionUtilitiesKit/Database/StorageError.swift b/SessionUtilitiesKit/Database/StorageError.swift index 7bd661487..5094ee875 100644 --- a/SessionUtilitiesKit/Database/StorageError.swift +++ b/SessionUtilitiesKit/Database/StorageError.swift @@ -9,6 +9,8 @@ public enum StorageError: Error { case migrationFailed case migrationNoLongerSupported case invalidKeySpec + case keySpecCreationFailed + case keySpecInaccessible case decodingFailed case failedToSave diff --git a/SessionUtilitiesKit/Database/Types/Migration.swift b/SessionUtilitiesKit/Database/Types/Migration.swift index 6e4c909e5..aa8a815de 100644 --- a/SessionUtilitiesKit/Database/Types/Migration.swift +++ b/SessionUtilitiesKit/Database/Types/Migration.swift @@ -8,17 +8,21 @@ public protocol Migration { static var identifier: String { get } static var needsConfigSync: Bool { get } static var minExpectedRunDuration: TimeInterval { get } + static var requirements: [MigrationRequirement] { get } static func migrate(_ db: Database) throws } public extension Migration { + static var requirements: [MigrationRequirement] { [] } + static func loggedMigrate( _ storage: Storage?, targetIdentifier: TargetMigrations.Identifier ) -> ((_ db: Database) throws -> ()) { return { (db: Database) in SNLogNotTests("[Migration Info] Starting \(targetIdentifier.key(with: self))") + storage?.willStartMigration(db, self) storage?.internalCurrentlyRunningMigration.mutate { $0 = (targetIdentifier, self) } defer { storage?.internalCurrentlyRunningMigration.mutate { $0 = nil } } diff --git a/SessionUtilitiesKit/Database/Types/MigrationRequirement.swift b/SessionUtilitiesKit/Database/Types/MigrationRequirement.swift new file mode 100644 index 000000000..9e586b809 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/MigrationRequirement.swift @@ -0,0 +1,12 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +import Foundation + +public enum MigrationRequirement: CaseIterable { + case sessionUtilStateLoaded + + var shouldProcessAtCompletionIfNotRequired: Bool { + switch self { + case .sessionUtilStateLoaded: return true + } + } +} diff --git a/SessionUtilitiesKit/General/AppContext.h b/SessionUtilitiesKit/General/AppContext.h index 82c90d809..dff051bd0 100755 --- a/SessionUtilitiesKit/General/AppContext.h +++ b/SessionUtilitiesKit/General/AppContext.h @@ -2,15 +2,6 @@ NS_ASSUME_NONNULL_BEGIN -static inline BOOL OWSIsDebugBuild() -{ -#ifdef DEBUG - return YES; -#else - return NO; -#endif -} - // These are fired whenever the corresponding "main app" or "app extension" // notification is fired. // diff --git a/SessionUtilitiesKit/General/SNUserDefaults.swift b/SessionUtilitiesKit/General/SNUserDefaults.swift index 7564c9753..a1829aab0 100644 --- a/SessionUtilitiesKit/General/SNUserDefaults.swift +++ b/SessionUtilitiesKit/General/SNUserDefaults.swift @@ -61,8 +61,10 @@ public enum SNUserDefaults { } public extension UserDefaults { + public static let applicationGroup: String = "group.com.loki-project.loki-messenger" + @objc static var sharedLokiProject: UserDefaults? { - UserDefaults(suiteName: "group.com.loki-project.loki-messenger") + UserDefaults(suiteName: UserDefaults.applicationGroup) } } diff --git a/SessionUtilitiesKit/General/SessionId.swift b/SessionUtilitiesKit/General/SessionId.swift index ecf4bc3a5..25cc06def 100644 --- a/SessionUtilitiesKit/General/SessionId.swift +++ b/SessionUtilitiesKit/General/SessionId.swift @@ -11,6 +11,7 @@ public struct SessionId { case blinded15 = "15" // Used for authentication and participants in open groups with blinding enabled case blinded25 = "25" // Used for authentication and participants in open groups with blinding enabled case unblinded = "00" // Used for authentication in open groups with blinding disabled + case group = "03" // Used for update group conversations public init?(from stringValue: String?) { guard let stringValue: String = stringValue else { return nil } diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index b118c0571..7c46c2730 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -197,6 +197,7 @@ public final class JobRunner: JobRunnerType { self.blockingQueue = Atomic( JobQueue( type: .blocking, + executionType: .serial, qos: .default, isTestingJobRunner: isTestingJobRunner, jobVariants: [] @@ -242,6 +243,7 @@ public final class JobRunner: JobRunnerType { JobQueue( type: .attachmentDownload, + executionType: .serial, qos: .utility, isTestingJobRunner: isTestingJobRunner, jobVariants: [ @@ -253,6 +255,7 @@ public final class JobRunner: JobRunnerType { JobQueue( type: .general(number: 0), + executionType: .serial, qos: .utility, isTestingJobRunner: isTestingJobRunner, jobVariants: Array(jobVariants) @@ -678,26 +681,6 @@ public final class JobRunner: JobRunnerType { queues.wrappedValue[job.variant]?.removePendingJob(jobId) } - - //public static func hasPendingOrRunningJob( - // with variant: Job.Variant, - // threadId: String? = nil, - // interactionId: Int64? = nil, - // details: T? = nil - //) -> Bool { - // guard let targetQueue: JobQueue = queues.wrappedValue[variant] else { return false } - // - // // Ensure we can encode the details (if provided) - // let detailsData: Data? = details.map { try? JSONEncoder().encode($0) } - // - // guard details == nil || detailsData != nil else { return false } - // - // return targetQueue.hasPendingOrRunningJobWith( - // threadId: threadId, - // interactionId: interactionId, - // detailsData: detailsData - // ) - //} // MARK: - Convenience @@ -848,7 +831,7 @@ public final class JobQueue: Hashable { fileprivate init( type: QueueType, - executionType: ExecutionType = .serial, + executionType: ExecutionType, qos: DispatchQoS, isTestingJobRunner: Bool, jobVariants: [Job.Variant] diff --git a/SessionUtilitiesKit/Utilities/Bencode.swift b/SessionUtilitiesKit/Utilities/Bencode.swift new file mode 100644 index 000000000..1138208cc --- /dev/null +++ b/SessionUtilitiesKit/Utilities/Bencode.swift @@ -0,0 +1,263 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol BencodableType { + associatedtype ValueType: BencodableType + + static var isCollection: Bool { get } + static var isDictionary: Bool { get } +} + +public struct BencodeResponse { + public let info: T + public let data: Data? +} + +extension BencodeResponse: Equatable where T: Equatable {} + +public enum Bencode { + private enum Element: Character { + case number0 = "0" + case number1 = "1" + case number2 = "2" + case number3 = "3" + case number4 = "4" + case number5 = "5" + case number6 = "6" + case number7 = "7" + case number8 = "8" + case number9 = "9" + case intIndicator = "i" + case listIndicator = "l" + case dictIndicator = "d" + case endIndicator = "e" + case separator = ":" + + init?(_ byte: UInt8?) { + guard + let byte: UInt8 = byte, + let byteString: String = String(data: Data([byte]), encoding: .utf8), + let character: Character = byteString.first, + let result: Element = Element(rawValue: character) + else { return nil } + + self = result + } + } + + private struct BencodeString { + let value: String? + let rawValue: Data + } + + // MARK: - Functions + + public static func decodeResponse( + from data: Data, + using dependencies: Dependencies = Dependencies() + ) throws -> BencodeResponse where T: Decodable { + guard + let result: [Data] = try? decode([Data].self, from: data), + let responseData: Data = result.first + else { throw HTTPError.parsingFailed } + + return BencodeResponse( + info: try responseData.decoded(as: T.self, using: dependencies), + data: (result.count > 1 ? result.last : nil) + ) + } + + public static func decode(_ type: T.Type, from data: Data) throws -> T { + guard + let decodedData: (value: Any, remainingData: Data) = decodeData(data), + decodedData.remainingData.isEmpty == true // Ensure there is no left over data + else { throw HTTPError.parsingFailed } + + return try recursiveCast(type, from: decodedData.value) + } + + // MARK: - Logic + + private static func decodeData(_ data: Data) -> (value: Any, remainingData: Data)? { + switch Element(data.first) { + case .number0, .number1, .number2, .number3, .number4, + .number5, .number6, .number7, .number8, .number9: + return decodeString(data) + + case .intIndicator: return decodeInt(data) + case .listIndicator: return decodeList(data) + case .dictIndicator: return decodeDict(data) + default: return nil + } + } + + /// Decode a string element from iterator assumed to have structure `{length}:{data}` + private static func decodeString(_ data: Data) -> (value: BencodeString, remainingData: Data)? { + var mutableData: Data = data + var lengthData: [UInt8] = [] + + // Remove bytes until we hit the separator + while let next: UInt8 = mutableData.popFirst(), Element(next) != .separator { + lengthData.append(next) + } + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + mutableData = Data(mutableData) + + guard + let lengthString: String = String(data: Data(lengthData), encoding: .ascii), + let length: Int = Int(lengthString, radix: 10), + mutableData.count >= length + else { return nil } + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return ( + BencodeString( + value: String(data: mutableData[0.. (value: Int, remainingData: Data)? { + var mutableData: Data = data + var intData: [UInt8] = [] + _ = mutableData.popFirst() // drop `i` + + // Pop until after `e` + while let next: UInt8 = mutableData.popFirst(), Element(next) != .endIndicator { + intData.append(next) + } + + guard + let intString: String = String(data: Data(intData), encoding: .ascii), + let result: Int = Int(intString, radix: 10) + else { return nil } + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return (result, Data(mutableData)) + } + + /// Decode a list element from iterator assumed to have structure `l{data}e` + private static func decodeList(_ data: Data) -> ([Any], Data)? { + var mutableData: Data = data + var listElements: [Any] = [] + _ = mutableData.popFirst() // drop `l` + + while !mutableData.isEmpty, let next: UInt8 = mutableData.first, Element(next) != .endIndicator { + guard let result = decodeData(mutableData) else { break } + + listElements.append(result.value) + mutableData = result.remainingData + } + + _ = mutableData.popFirst() // drop `e` + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return (listElements, Data(mutableData)) + } + + /// Decode a dict element from iterator assumed to have structure `d{data}e` + private static func decodeDict(_ data: Data) -> ([String: Any], Data)? { + var mutableData: Data = data + var dictElements: [String: Any] = [:] + _ = mutableData.popFirst() // drop `d` + + while !mutableData.isEmpty, let next: UInt8 = mutableData.first, Element(next) != .endIndicator { + guard + let keyResult = decodeString(mutableData), + let key: String = keyResult.value.value, + let valueResult = decodeData(keyResult.remainingData) + else { return nil } + + dictElements[key] = valueResult.value + mutableData = valueResult.remainingData + } + + _ = mutableData.popFirst() // drop `e` + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return (dictElements, Data(mutableData)) + } + + // MARK: - Internal Functions + + private static func recursiveCast(_ type: T.Type, from value: Any) throws -> T { + switch (type.isCollection, type.isDictionary) { + case (_, true): + guard let dictValue: [String: Any] = value as? [String: Any] else { throw HTTPError.parsingFailed } + + return try ( + dictValue.mapValues { try recursiveCast(type.ValueType.self, from: $0) } as? T ?? + { throw HTTPError.parsingFailed }() + ) + + case (true, _): + guard let arrayValue: [Any] = value as? [Any] else { throw HTTPError.parsingFailed } + + return try ( + arrayValue.map { try recursiveCast(type.ValueType.self, from: $0) } as? T ?? + { throw HTTPError.parsingFailed }() + ) + + default: + switch (value, type) { + case (let bencodeString as BencodeString, is String.Type): + return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }()) + + case (let bencodeString as BencodeString, is Optional.Type): + return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }()) + + case (let bencodeString as BencodeString, _): + return try (bencodeString.rawValue as? T ?? { throw HTTPError.parsingFailed }()) + + default: return try (value as? T ?? { throw HTTPError.parsingFailed }()) + } + } + } +} + +// MARK: - BencodableType Extensions + +extension Data: BencodableType { + public typealias ValueType = Data + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { false } +} + +extension Int: BencodableType { + public typealias ValueType = Int + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { false } +} + +extension String: BencodableType { + public typealias ValueType = String + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { false } +} + +extension Array: BencodableType where Element: BencodableType { + public typealias ValueType = Element + + public static var isCollection: Bool { true } + public static var isDictionary: Bool { false } +} + +extension Dictionary: BencodableType where Key == String, Value: BencodableType { + public typealias ValueType = Value + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { true } +} diff --git a/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift new file mode 100644 index 000000000..08f00df10 --- /dev/null +++ b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift @@ -0,0 +1,97 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionUtilitiesKit + +class BencodeSpec: QuickSpec { + struct TestType: Codable, Equatable { + let intValue: Int + let stringValue: String + } + + // MARK: - Spec + + override func spec() { + describe("Bencode") { + context("when decoding") { + it("should decode a basic string") { + let basicStringData: Data = "5:howdy".data(using: .utf8)! + let result = try? Bencode.decode(String.self, from: basicStringData) + + expect(result).to(equal("howdy")) + } + + it("should decode a basic integer") { + let basicIntegerData: Data = "i3e".data(using: .utf8)! + let result = try? Bencode.decode(Int.self, from: basicIntegerData) + + expect(result).to(equal(3)) + } + + it("should decode a list of integers") { + let basicIntListData: Data = "li1ei2ee".data(using: .utf8)! + let result = try? Bencode.decode([Int].self, from: basicIntListData) + + expect(result).to(equal([1, 2])) + } + + it("should decode a basic dict") { + let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)! + let result = try? Bencode.decode([String: [String]].self, from: basicDictData) + + expect(result).to(equal(["spam": ["a", "b"]])) + } + } + + context("when decoding a response") { + it("decodes successfully") { + let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" + .data(using: .utf8)! + let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) + + expect(result) + .to(equal( + BencodeResponse( + info: TestType( + intValue: 100, + stringValue: "Test" + ), + data: Data([1, 2, 3, 4, 5]) + ) + )) + } + + it("decodes successfully with no body") { + let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e" + .data(using: .utf8)! + let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) + + expect(result) + .to(equal( + BencodeResponse( + info: TestType( + intValue: 100, + stringValue: "Test" + ), + data: nil + ) + )) + } + + it("throws a parsing error when invalid") { + let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" + .data(using: .utf8)! + + expect { + let result: BencodeResponse = try Bencode.decodeResponse(from: data) + _ = result + }.to(throwError(HTTPError.parsingFailed)) + } + } + } + } +} diff --git a/SignalUtilitiesKit/Configuration.swift b/SignalUtilitiesKit/Configuration.swift index 4ab7e2d50..651bd49cd 100644 --- a/SignalUtilitiesKit/Configuration.swift +++ b/SignalUtilitiesKit/Configuration.swift @@ -4,6 +4,7 @@ import Foundation import SessionUIKit import SessionSnodeKit import SessionMessagingKit +import SessionUtilitiesKit public enum Configuration { public static func performMainSetup() { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 39f6c73f5..a6b1a8782 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -9,6 +9,7 @@ import CoreServices import SessionUIKit import SessionMessagingKit import SignalCoreKit +import SessionUtilitiesKit public protocol AttachmentApprovalViewControllerDelegate: AnyObject { func attachmentApproval( diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentCaptionToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentCaptionToolbar.swift index 4c3ad9f57..dcd4f2044 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentCaptionToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentCaptionToolbar.swift @@ -4,6 +4,7 @@ import Foundation import UIKit import SessionUIKit import SignalCoreKit +import SessionUtilitiesKit protocol AttachmentCaptionToolbarDelegate: AnyObject { func attachmentCaptionToolbarDidEdit(_ attachmentCaptionToolbar: AttachmentCaptionToolbar) @@ -150,26 +151,7 @@ class AttachmentCaptionToolbar: UIView, UITextViewDelegate { public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { let existingText: String = textView.text ?? "" let proposedText: String = (existingText as NSString).replacingCharacters(in: range, with: text) - - // Don't complicate things by mixing media attachments with oversize text attachments - guard proposedText.utf8.count < kOversizeTextMessageSizeThreshold else { - Logger.debug("long text was truncated") - self.lengthLimitLabel.isHidden = false - - // `range` represents the section of the existing text we will replace. We can re-use that space. - // Range is in units of NSStrings's standard UTF-16 characters. Since some of those chars could be - // represented as single bytes in utf-8, while others may be 8 or more, the only way to be sure is - // to just measure the utf8 encoded bytes of the replaced substring. - let bytesAfterDelete: Int = (existingText as NSString).replacingCharacters(in: range, with: "").utf8.count - - // Accept as much of the input as we can - let byteBudget: Int = Int(kOversizeTextMessageSizeThreshold) - bytesAfterDelete - if byteBudget >= 0, let acceptableNewText = text.truncated(toByteCount: UInt(byteBudget)) { - textView.text = (existingText as NSString).replacingCharacters(in: range, with: acceptableNewText) - } - - return false - } + self.lengthLimitLabel.isHidden = true // After verifying the byte-length is sufficiently small, verify the character count is within bounds. diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index db65cd7e2..4b45d2e1d 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -5,6 +5,8 @@ import UIKit import AVFoundation import SessionUIKit import SignalCoreKit +import SessionMessagingKit +import SessionUtilitiesKit protocol AttachmentPrepViewControllerDelegate: AnyObject { func prepViewControllerUpdateNavigationBar() diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift index 79ac67d63..736c9049b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift @@ -2,9 +2,10 @@ import Foundation import UIKit -import SessionUIKit -import SignalCoreKit import PureLayout +import SignalCoreKit +import SessionUIKit +import SessionUtilitiesKit // Coincides with Android's max text message length let kMaxMessageBodyCharacterCount = 2000 @@ -228,25 +229,6 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { let existingText: String = textView.text ?? "" let proposedText: String = (existingText as NSString).replacingCharacters(in: range, with: text) - // Don't complicate things by mixing media attachments with oversize text attachments - guard proposedText.utf8.count < kOversizeTextMessageSizeThreshold else { - Logger.debug("long text was truncated") - self.lengthLimitLabel.isHidden = false - - // `range` represents the section of the existing text we will replace. We can re-use that space. - // Range is in units of NSStrings's standard UTF-16 characters. Since some of those chars could be - // represented as single bytes in utf-8, while others may be 8 or more, the only way to be sure is - // to just measure the utf8 encoded bytes of the replaced substring. - let bytesAfterDelete: Int = (existingText as NSString).replacingCharacters(in: range, with: "").utf8.count - - // Accept as much of the input as we can - let byteBudget: Int = Int(kOversizeTextMessageSizeThreshold) - bytesAfterDelete - if byteBudget >= 0, let acceptableNewText = text.truncated(toByteCount: UInt(byteBudget)) { - textView.text = (existingText as NSString).replacingCharacters(in: range, with: acceptableNewText) - } - - return false - } self.lengthLimitLabel.isHidden = true // After verifying the byte-length is sufficiently small, verify the character count is within bounds. diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift index e0acf8bba..903c886cf 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SignalCoreKit +import SessionUtilitiesKit public class EditorTextLayer: CATextLayer { let itemId: String diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift index ad415e5e6..c2f3585fe 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SignalCoreKit +import SessionUtilitiesKit public protocol ImageEditorCropViewControllerDelegate: AnyObject { func cropDidComplete(transform: ImageEditorTransform) @@ -200,7 +201,7 @@ class ImageEditorCropViewController: OWSViewController { case .topRight, .bottomRight: cropCornerView.autoPinEdge(toSuperviewEdge: .right) default: - owsFailDebug("Invalid crop region: \(cropRegion)") + owsFailDebug("Invalid crop region: \(String(describing: cropRegion))") } switch cropCornerView.cropRegion { case .topLeft, .topRight: @@ -208,7 +209,7 @@ class ImageEditorCropViewController: OWSViewController { case .bottomLeft, .bottomRight: cropCornerView.autoPinEdge(toSuperviewEdge: .bottom) default: - owsFailDebug("Invalid crop region: \(cropRegion)") + owsFailDebug("Invalid crop region: \(String(describing: cropRegion))") } } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift index d2c8b062b..9ac29b78b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift @@ -2,6 +2,7 @@ import UIKit import SignalCoreKit +import SessionUtilitiesKit // Used to represent undo/redo operations. // diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPinchGestureRecognizer.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPinchGestureRecognizer.swift index 01e580ecb..8d6551f4b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPinchGestureRecognizer.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPinchGestureRecognizer.swift @@ -2,6 +2,7 @@ import UIKit import SignalCoreKit +import SessionUtilitiesKit public struct ImageEditorPinchState { public let centroid: CGPoint diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorStrokeItem.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorStrokeItem.swift index f86940d22..50b627eea 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorStrokeItem.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorStrokeItem.swift @@ -3,6 +3,7 @@ // import UIKit +import SessionUtilitiesKit @objc public class ImageEditorStrokeItem: ImageEditorItem { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 6089077cc..f0be90aec 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -8,6 +8,7 @@ import NVActivityIndicatorView import SessionUIKit import SessionMessagingKit import SignalCoreKit +import SessionUtilitiesKit public protocol MediaMessageViewAudioDelegate: AnyObject { func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat) diff --git a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h index b856fa9a0..73cd95cff 100644 --- a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h +++ b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h @@ -3,15 +3,5 @@ FOUNDATION_EXPORT double SignalUtilitiesKitVersionNumber; FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[]; -@import SessionMessagingKit; -@import SessionSnodeKit; -@import SessionUtilitiesKit; - #import -#import -#import -#import -#import -#import #import -#import diff --git a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift b/SignalUtilitiesKit/Screen Lock/ScreenLock.swift index c1ed4654a..c87fb370d 100644 --- a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift +++ b/SignalUtilitiesKit/Screen Lock/ScreenLock.swift @@ -7,6 +7,10 @@ import SessionMessagingKit import SignalCoreKit public class ScreenLock { + public enum ScreenLockError: Error { + case general(description: String) + } + public enum Outcome { case success case cancel @@ -54,11 +58,11 @@ public class ScreenLock { switch outcome { case .failure(let error): Logger.error("local authentication failed with error: \(error)") - failure(self.authenticationError(errorDescription: error)) + failure(ScreenLockError.general(description: error)) case .unexpectedFailure(let error): Logger.error("local authentication failed with unexpected error: \(error)") - unexpectedFailure(self.authenticationError(errorDescription: error)) + unexpectedFailure(ScreenLockError.general(description: error)) case .success: Logger.verbose("local authentication succeeded.") @@ -203,11 +207,7 @@ public class ScreenLock { } } - return .failure(error:defaultErrorDescription) - } - - private func authenticationError(errorDescription: String) -> Error { - return OWSErrorWithCodeDescription(.localAuthenticationError, errorDescription) + return .failure(error: defaultErrorDescription) } // MARK: - Context diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index f656ba41d..75b708cba 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -5,6 +5,7 @@ import GRDB import SessionMessagingKit import SessionUtilitiesKit import SessionUIKit +import SessionSnodeKit public enum AppSetup { private static let hasRun: Atomic = Atomic(false) @@ -63,6 +64,14 @@ public enum AppSetup { migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationsCompletion: @escaping (Result, Bool) -> () ) { + // If the database can't be initialised into a valid state then error + guard Storage.shared.isValid else { + DispatchQueue.main.async { + migrationsCompletion(Result.failure(StorageError.databaseInvalid), false) + } + return + } + var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function)) Storage.shared.perform( @@ -73,6 +82,20 @@ public enum AppSetup { SNUIKit.self ], onProgressUpdate: migrationProgressChanged, + onMigrationRequirement: { db, requirement in + switch requirement { + case .sessionUtilStateLoaded: + guard Identity.userExists(db) else { return } + + // After the migrations have run but before the migration completion we load the + // SessionUtil state + SessionUtil.loadState( + db, + userPublicKey: getUserHexEncodedPublicKey(db), + ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey + ) + } + }, onComplete: { result, needsConfigSync in // After the migrations have run but before the migration completion we load the // SessionUtil state and update the 'needsConfigSync' flag based on whether the @@ -84,6 +107,8 @@ public enum AppSetup { ) } + // The 'needsConfigSync' flag should be based on whether either a migration or the + // configs need to be sync'ed migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync)) // The 'if' is only there to prevent the "variable never read" warning from showing diff --git a/SignalUtilitiesKit/Utilities/ByteParser.h b/SignalUtilitiesKit/Utilities/ByteParser.h deleted file mode 100644 index c30c8c86c..000000000 --- a/SignalUtilitiesKit/Utilities/ByteParser.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface ByteParser : NSObject - -@property (nonatomic, readonly) BOOL hasError; - -- (instancetype)init NS_UNAVAILABLE; - -- (instancetype)initWithData:(NSData *)data littleEndian:(BOOL)littleEndian; - -#pragma mark - Short - -- (uint16_t)shortAtIndex:(NSUInteger)index; -- (uint16_t)nextShort; - -#pragma mark - Int - -- (uint32_t)intAtIndex:(NSUInteger)index; -- (uint32_t)nextInt; - -#pragma mark - Long - -- (uint64_t)longAtIndex:(NSUInteger)index; -- (uint64_t)nextLong; - -#pragma mark - - -- (BOOL)readZero:(NSUInteger)length; - -- (nullable NSData *)readBytes:(NSUInteger)length; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/ByteParser.m b/SignalUtilitiesKit/Utilities/ByteParser.m deleted file mode 100644 index 4dd7c38db..000000000 --- a/SignalUtilitiesKit/Utilities/ByteParser.m +++ /dev/null @@ -1,143 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "ByteParser.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface ByteParser () - -@property (nonatomic, readonly) BOOL littleEndian; -@property (nonatomic, readonly) NSData *data; -@property (nonatomic) NSUInteger cursor; -@property (nonatomic) BOOL hasError; - -@end - -#pragma mark - - -@implementation ByteParser - -- (instancetype)initWithData:(NSData *)data littleEndian:(BOOL)littleEndian -{ - if (self = [super init]) { - _littleEndian = littleEndian; - _data = data; - } - - return self; -} - -#pragma mark - Short - -- (uint16_t)shortAtIndex:(NSUInteger)index -{ - uint16_t value; - const size_t valueSize = sizeof(value); - OWSAssertDebug(valueSize == 2); - if (index + valueSize > self.data.length) { - self.hasError = YES; - return 0; - } - [self.data getBytes:&value range:NSMakeRange(index, valueSize)]; - if (self.littleEndian) { - return CFSwapInt16LittleToHost(value); - } else { - return CFSwapInt16BigToHost(value); - } -} - -- (uint16_t)nextShort -{ - uint16_t value = [self shortAtIndex:self.cursor]; - self.cursor += sizeof(value); - return value; -} - -#pragma mark - Int - -- (uint32_t)intAtIndex:(NSUInteger)index -{ - uint32_t value; - const size_t valueSize = sizeof(value); - OWSAssertDebug(valueSize == 4); - if (index + valueSize > self.data.length) { - self.hasError = YES; - return 0; - } - [self.data getBytes:&value range:NSMakeRange(index, valueSize)]; - if (self.littleEndian) { - return CFSwapInt32LittleToHost(value); - } else { - return CFSwapInt32BigToHost(value); - } -} - -- (uint32_t)nextInt -{ - uint32_t value = [self intAtIndex:self.cursor]; - self.cursor += sizeof(value); - return value; -} - -#pragma mark - Long - -- (uint64_t)longAtIndex:(NSUInteger)index -{ - uint64_t value; - const size_t valueSize = sizeof(value); - OWSAssertDebug(valueSize == 8); - if (index + valueSize > self.data.length) { - self.hasError = YES; - return 0; - } - [self.data getBytes:&value range:NSMakeRange(index, valueSize)]; - if (self.littleEndian) { - return CFSwapInt64LittleToHost(value); - } else { - return CFSwapInt64BigToHost(value); - } -} - -- (uint64_t)nextLong -{ - uint64_t value = [self longAtIndex:self.cursor]; - self.cursor += sizeof(value); - return value; -} - -#pragma mark - - -- (BOOL)readZero:(NSUInteger)length -{ - NSData *_Nullable subdata = [self readBytes:length]; - if (!subdata) { - return NO; - } - uint8_t bytes[length]; - [subdata getBytes:bytes range:NSMakeRange(0, length)]; - for (int i = 0; i < length; i++) { - if (bytes[i] != 0) { - return NO; - } - } - return YES; -} - -- (nullable NSData *)readBytes:(NSUInteger)length -{ - NSUInteger index = self.cursor; - if (index + length > self.data.length) { - self.hasError = YES; - return nil; - } - NSData *_Nullable subdata = [self.data subdataWithRange:NSMakeRange(index, length)]; - self.cursor += length; - return subdata; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/FunctionalUtil.h b/SignalUtilitiesKit/Utilities/FunctionalUtil.h deleted file mode 100644 index e86ed911a..000000000 --- a/SignalUtilitiesKit/Utilities/FunctionalUtil.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface NSArray (FunctionalUtil) - -/// Returns true when any of the items in this array match the given predicate. -- (bool)any:(int (^)(id item))predicate; - -/// Returns true when all of the items in this array match the given predicate. -- (bool)all:(int (^)(id item))predicate; - -/// Returns an array of all the results of passing items from this array through the given projection function. -- (NSArray *)map:(id (^)(id item))projection; - -/// Returns an array of all the results of passing items from this array through the given projection function. -- (NSArray *)filter:(int (^)(id item))predicate; - -- (NSDictionary *)groupBy:(id (^)(id value))keySelector; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/FunctionalUtil.m b/SignalUtilitiesKit/Utilities/FunctionalUtil.m deleted file mode 100644 index 65fe6dc5c..000000000 --- a/SignalUtilitiesKit/Utilities/FunctionalUtil.m +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "FunctionalUtil.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FUBadArgument : NSException - -+ (FUBadArgument *) new:(NSString *)reason; -+ (void)raise:(NSString *)message; - -@end - -@implementation FUBadArgument - -+ (FUBadArgument *) new:(NSString *)reason { - return [[FUBadArgument alloc] initWithName:@"Invalid Argument" reason:reason userInfo:nil]; -} -+ (void)raise:(NSString *)message { - [FUBadArgument raise:@"Invalid Argument" format:@"%@", message]; -} - -@end - -#define tskit_require(expr) \ - if (!(expr)) { \ - NSString *reason = \ - [NSString stringWithFormat:@"require %@ (in %s at line %d)", (@ #expr), __FILE__, __LINE__]; \ - OWSLogError(@"%@", reason); \ - [FUBadArgument raise:reason]; \ - }; - - -@implementation NSArray (FunctionalUtil) -- (bool)any:(int (^)(id item))predicate { - tskit_require(predicate != nil); - for (id e in self) { - if (predicate(e)) { - return true; - } - } - return false; -} -- (bool)all:(int (^)(id item))predicate { - tskit_require(predicate != nil); - for (id e in self) { - if (!predicate(e)) { - return false; - } - } - return true; -} -- (NSArray *)map:(id (^)(id item))projection { - tskit_require(projection != nil); - - NSMutableArray *r = [NSMutableArray arrayWithCapacity:self.count]; - for (id e in self) { - [r addObject:projection(e)]; - } - return r; -} -- (NSArray *)filter:(int (^)(id item))predicate { - tskit_require(predicate != nil); - - NSMutableArray *r = [NSMutableArray array]; - for (id e in self) { - if (predicate(e)) { - [r addObject:e]; - } - } - return r; -} - -- (NSDictionary *)groupBy:(id (^)(id value))keySelector { - tskit_require(keySelector != nil); - - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - - for (id item in self) { - id key = keySelector(item); - - NSMutableArray *group = result[key]; - if (group == nil) { - group = [NSMutableArray array]; - result[key] = group; - } - [group addObject:item]; - } - - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/NSURLSessionDataTask+StatusCode.h b/SignalUtilitiesKit/Utilities/NSURLSessionDataTask+StatusCode.h deleted file mode 100644 index 62718ffe3..000000000 --- a/SignalUtilitiesKit/Utilities/NSURLSessionDataTask+StatusCode.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface NSURLSessionTask (StatusCode) - -- (long)statusCode; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/NSURLSessionDataTask+StatusCode.m b/SignalUtilitiesKit/Utilities/NSURLSessionDataTask+StatusCode.m deleted file mode 100644 index 212eeac55..000000000 --- a/SignalUtilitiesKit/Utilities/NSURLSessionDataTask+StatusCode.m +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "NSURLSessionDataTask+StatusCode.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation NSURLSessionTask (StatusCode) - -- (long)statusCode { - NSHTTPURLResponse *response = (NSHTTPURLResponse *)self.response; - return response.statusCode; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/OWSError.h b/SignalUtilitiesKit/Utilities/OWSError.h deleted file mode 100644 index e4772fc17..000000000 --- a/SignalUtilitiesKit/Utilities/OWSError.h +++ /dev/null @@ -1,69 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -extern NSString *const OWSSignalServiceKitErrorDomain; - -typedef NS_ENUM(NSInteger, OWSErrorCode) { - OWSErrorCodeInvalidMethodParameters = 11, - OWSErrorCodeUnableToProcessServerResponse = 12, - OWSErrorCodeFailedToDecodeJson = 13, - OWSErrorCodeFailedToEncodeJson = 14, - OWSErrorCodeFailedToDecodeQR = 15, - OWSErrorCodePrivacyVerificationFailure = 20, - OWSErrorCodeUntrustedIdentity = 25, - OWSErrorCodeFailedToSendOutgoingMessage = 30, - OWSErrorCodeAssertionFailure = 31, - OWSErrorCodeFailedToDecryptMessage = 100, - OWSErrorCodeFailedToDecryptUDMessage = 101, - OWSErrorCodeFailedToEncryptMessage = 110, - OWSErrorCodeFailedToEncryptUDMessage = 111, - OWSErrorCodeSignalServiceFailure = 1001, - OWSErrorCodeSignalServiceRateLimited = 1010, - OWSErrorCodeUserError = 2001, - OWSErrorCodeMessageSendDisabledDueToPreKeyUpdateFailures = 777405, - OWSErrorCodeMessageSendFailedToBlockList = 777406, - OWSErrorCodeMessageSendNoValidRecipients = 777407, - OWSErrorCodeContactsUpdaterRateLimit = 777408, - OWSErrorCodeCouldNotWriteAttachmentData = 777409, - OWSErrorCodeMessageDeletedBeforeSent = 777410, - OWSErrorCodeDatabaseConversionFatalError = 777411, - OWSErrorCodeMoveFileToSharedDataContainerError = 777412, - OWSErrorCodeRegistrationMissing2FAPIN = 777413, - OWSErrorCodeDebugLogUploadFailed = 777414, - // A non-recoverable error occured while exporting a backup. - OWSErrorCodeExportBackupFailed = 777415, - // A possibly recoverable error occured while exporting a backup. - OWSErrorCodeExportBackupError = 777416, - // A non-recoverable error occured while importing a backup. - OWSErrorCodeImportBackupFailed = 777417, - // A possibly recoverable error occured while importing a backup. - OWSErrorCodeImportBackupError = 777418, - // A non-recoverable while importing or exporting a backup. - OWSErrorCodeBackupFailure = 777419, - OWSErrorCodeLocalAuthenticationError = 777420, - OWSErrorCodeMessageRequestFailed = 777421, - OWSErrorCodeMessageResponseFailed = 777422, - OWSErrorCodeInvalidMessage = 777423, - OWSErrorCodeProfileUpdateFailed = 777424, - OWSErrorCodeAvatarWriteFailed = 777425, - OWSErrorCodeAvatarUploadFailed = 777426, - OWSErrorCodeNoSessionForTransientMessage, -}; - -extern NSString *const OWSErrorRecipientIdentifierKey; - -extern NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description); -extern NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSString *recipientId); -extern NSError *OWSErrorMakeUnableToProcessServerResponseError(void); -extern NSError *OWSErrorMakeFailedToSendOutgoingMessageError(void); -extern NSError *OWSErrorMakeAssertionError(NSString *description); -extern NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(void); -extern NSError *OWSErrorMakeMessageSendFailedDueToBlockListError(void); -extern NSError *OWSErrorMakeWriteAttachmentDataError(void); - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/OWSError.m b/SignalUtilitiesKit/Utilities/OWSError.m deleted file mode 100644 index f8096d70e..000000000 --- a/SignalUtilitiesKit/Utilities/OWSError.m +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSError.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -NSString *const OWSSignalServiceKitErrorDomain = @"OWSSignalServiceKitErrorDomain"; -NSString *const OWSErrorRecipientIdentifierKey = @"OWSErrorKeyRecipientIdentifier"; - -NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description) -{ - return [NSError errorWithDomain:OWSSignalServiceKitErrorDomain - code:code - userInfo:@{ NSLocalizedDescriptionKey: description }]; -} - -NSError *OWSErrorMakeUnableToProcessServerResponseError() -{ - return OWSErrorWithCodeDescription(OWSErrorCodeUnableToProcessServerResponse, - NSLocalizedString(@"ERROR_DESCRIPTION_SERVER_FAILURE", @"Generic server error")); -} - -NSError *OWSErrorMakeFailedToSendOutgoingMessageError() -{ - return OWSErrorWithCodeDescription(OWSErrorCodeFailedToSendOutgoingMessage, - NSLocalizedString(@"ERROR_DESCRIPTION_CLIENT_SENDING_FAILURE", @"Generic notice when message failed to send.")); -} - -NSError *OWSErrorMakeAssertionError(NSString *description) -{ - OWSCFailDebug(@"Assertion failed: %@", description); - return OWSErrorWithCodeDescription(OWSErrorCodeAssertionFailure, - NSLocalizedString(@"ERROR_DESCRIPTION_UNKNOWN_ERROR", @"Worst case generic error message")); -} - -NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSString *recipientId) -{ - return [NSError - errorWithDomain:OWSSignalServiceKitErrorDomain - code:OWSErrorCodeUntrustedIdentity - userInfo:@{ NSLocalizedDescriptionKey : description, OWSErrorRecipientIdentifierKey : recipientId }]; -} - -NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError() -{ - return OWSErrorWithCodeDescription(OWSErrorCodeMessageSendDisabledDueToPreKeyUpdateFailures, - NSLocalizedString(@"ERROR_DESCRIPTION_MESSAGE_SEND_DISABLED_PREKEY_UPDATE_FAILURES", - @"Error message indicating that message send is disabled due to prekey update failures")); -} - -NSError *OWSErrorMakeMessageSendFailedDueToBlockListError() -{ - return OWSErrorWithCodeDescription(OWSErrorCodeMessageSendFailedToBlockList, - NSLocalizedString(@"ERROR_DESCRIPTION_MESSAGE_SEND_FAILED_DUE_TO_BLOCK_LIST", - @"Error message indicating that message send failed due to block list")); -} - -NSError *OWSErrorMakeWriteAttachmentDataError() -{ - return OWSErrorWithCodeDescription(OWSErrorCodeCouldNotWriteAttachmentData, - NSLocalizedString(@"ERROR_DESCRIPTION_MESSAGE_SEND_FAILED_DUE_TO_FAILED_ATTACHMENT_WRITE", - @"Error message indicating that message send failed due to failed attachment write")); -} - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/OWSOperation.h b/SignalUtilitiesKit/Utilities/OWSOperation.h deleted file mode 100644 index ebeeaa53f..000000000 --- a/SignalUtilitiesKit/Utilities/OWSOperation.h +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSInteger, OWSOperationState) { - OWSOperationStateNew, - OWSOperationStateExecuting, - OWSOperationStateFinished -}; - -// A base class for implementing retryable operations. -// To utilize the retryable behavior: -// Set remainingRetries to something greater than 0, and when you're reporting an error, -// set `error.isRetryable = YES`. -// If the failure is one that will not succeed upon retry, set `error.isFatal = YES`. -// -// isRetryable and isFatal are opposites but not redundant. -// -// If a group message send fails, the send will be retried if any of the errors were retryable UNLESS -// any of the errors were fatal. Fatal errors trump retryable errors. -@interface OWSOperation : NSOperation - -@property (readonly, nullable) NSError *failingError; - -// Defaults to 0, set to greater than 0 in init if you'd like the operation to be retryable. -@property NSUInteger remainingRetries; - -#pragma mark - Mandatory Subclass Overrides - -// Called every retry, this is where the bulk of the operation's work should go. -- (void)run; - -#pragma mark - Optional Subclass Overrides - -// Called one time only -- (nullable NSError *)checkForPreconditionError; - -// Called at most one time. -- (void)didSucceed; - -// Called at most one time. -- (void)didCancel; - -// Called zero or more times, retry may be possible -- (void)didReportError:(NSError *)error; - -// Called at most one time, once retry is no longer possible. -- (void)didFailWithError:(NSError *)error NS_SWIFT_NAME(didFail(error:)); - -// How long to wait before retry, if possible -- (NSTimeInterval)retryInterval; - -#pragma mark - Success/Error - Do Not Override - -// Runs now if a retry timer has been set by a previous failure, -// otherwise assumes we're currently running and does nothing. -- (void)runAnyQueuedRetry; - -// Report that the operation completed successfully. -// -// Each invocation of `run` must make exactly one call to one of: `reportSuccess`, `reportCancelled`, or `reportError:` -- (void)reportSuccess; - -// Call this when you abort before completion due to being cancelled. -// -// Each invocation of `run` must make exactly one call to one of: `reportSuccess`, `reportCancelled`, or `reportError:` -- (void)reportCancelled; - -// Report that the operation failed to complete due to an error. -// -// Each invocation of `run` must make exactly one call to one of: `reportSuccess`, `reportCancelled`, or `reportError:` -// You must ensure that `run` cannot succeed after calling `reportError`, e.g. generally you'll write something like -// this: -// -// [self reportError:someError]; -// return; -// -// If the error is terminal, and you want to avoid retry, report an error with `error.isFatal = YES` otherwise the -// operation will retry if possible. -- (void)reportError:(NSError *)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/OWSOperation.m b/SignalUtilitiesKit/Utilities/OWSOperation.m deleted file mode 100644 index 47e511990..000000000 --- a/SignalUtilitiesKit/Utilities/OWSOperation.m +++ /dev/null @@ -1,253 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "OWSOperation.h" -#import "OWSError.h" -#import -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -NSString *const OWSOperationKeyIsExecuting = @"isExecuting"; -NSString *const OWSOperationKeyIsFinished = @"isFinished"; - -@interface OWSOperation () - -@property (nullable) NSError *failingError; -@property (atomic) OWSOperationState operationState; -@property (nonatomic) OWSBackgroundTask *backgroundTask; - -// This property should only be accessed on the main queue. -@property (nonatomic) NSTimer *_Nullable retryTimer; - -@end - -@implementation OWSOperation - -- (instancetype)init -{ - self = [super init]; - if (!self) { - return self; - } - - _operationState = OWSOperationStateNew; - _backgroundTask = [OWSBackgroundTask backgroundTaskWithLabel:self.logTag]; - - // Operations are not retryable by default. - _remainingRetries = 0; - - return self; -} - -- (void)dealloc -{ - OWSLogDebug(@"in dealloc"); -} - -#pragma mark - Subclass Overrides - -// Called one time only -- (nullable NSError *)checkForPreconditionError -{ - // OWSOperation have a notion of failure, which is inferred by the presence of a `failingError`. - // - // By default, any failing dependency cascades that failure to it's dependent. - // If you'd like different behavior, override this method (`checkForPreconditionError`) without calling `super`. - for (NSOperation *dependency in self.dependencies) { - if (![dependency isKindOfClass:[OWSOperation class]]) { - // Native operations, like NSOperation and NSBlockOperation have no notion of "failure". - // So there's no `failingError` to cascade. - continue; - } - - OWSOperation *dependentOperation = (OWSOperation *)dependency; - - // Don't proceed if dependency failed - surface the dependency's error. - NSError *_Nullable dependencyError = dependentOperation.failingError; - if (dependencyError != nil) { - return dependencyError; - } - } - - return nil; -} - -// Called every retry, this is where the bulk of the operation's work should go. -- (void)run -{ - OWSAbstractMethod(); -} - -// Called at most one time. -- (void)didSucceed -{ - // no-op - // Override in subclass if necessary -} - -// Called at most one time. -- (void)didCancel -{ - // no-op - // Override in subclass if necessary -} - -// Called zero or more times, retry may be possible -- (void)didReportError:(NSError *)error -{ - // no-op - // Override in subclass if necessary -} - -// Called at most one time, once retry is no longer possible. -- (void)didFailWithError:(NSError *)error -{ - // no-op - // Override in subclass if necessary -} - -#pragma mark - NSOperation overrides - -// Do not override this method in a subclass instead, override `run` -- (void)main -{ - OWSLogDebug(@"started."); - NSError *_Nullable preconditionError = [self checkForPreconditionError]; - if (preconditionError) { - [self failOperationWithError:preconditionError]; - return; - } - - if (self.isCancelled) { - [self reportCancelled]; - return; - } - - [self run]; -} - -- (void)runAnyQueuedRetry -{ - dispatch_async(dispatch_get_main_queue(), ^{ - NSTimer *_Nullable retryTimer = self.retryTimer; - self.retryTimer = nil; - [retryTimer invalidate]; - - if (retryTimer != nil) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self run]; - }); - } else { - OWSLogVerbose(@"not re-running since operation is already running."); - } - }); -} - -#pragma mark - Public Methods - -// These methods are not intended to be subclassed -- (void)reportSuccess -{ - OWSLogDebug(@"succeeded."); - [self didSucceed]; - [self markAsComplete]; -} - -// These methods are not intended to be subclassed -- (void)reportCancelled -{ - OWSLogDebug(@"cancelled."); - [self didCancel]; - [self markAsComplete]; -} - -- (void)reportError:(NSError *)error -{ - [self didReportError:error]; - - if (self.remainingRetries == 0) { - [self failOperationWithError:error]; - return; - } - - self.remainingRetries--; - - dispatch_async(dispatch_get_main_queue(), ^{ - OWSAssertDebug(self.retryTimer == nil); - [self.retryTimer invalidate]; - - // The `scheduledTimerWith*` methods add the timer to the current thread's RunLoop. - // Since Operations typically run on a background thread, that would mean the background - // thread's RunLoop. However, the OS can spin down background threads if there's no work - // being done, so we run the risk of the timer's RunLoop being deallocated before it's - // fired. - // - // To ensure the timer's thread sticks around, we schedule it while on the main RunLoop. - self.retryTimer = [NSTimer weakScheduledTimerWithTimeInterval:self.retryInterval - target:self - selector:@selector(runAnyQueuedRetry) - userInfo:nil - repeats:NO]; - }); -} - -// Override in subclass if you want something more sophisticated, e.g. exponential backoff -- (NSTimeInterval)retryInterval -{ - return 0.1; -} - -#pragma mark - Life Cycle - -- (void)failOperationWithError:(NSError *)error -{ - OWSLogDebug(@"failed terminally."); - self.failingError = error; - - [self didFailWithError:error]; - [self markAsComplete]; -} - -- (BOOL)isExecuting -{ - return self.operationState == OWSOperationStateExecuting; -} - -- (BOOL)isFinished -{ - return self.operationState == OWSOperationStateFinished; -} - -- (void)start -{ - [self willChangeValueForKey:OWSOperationKeyIsExecuting]; - self.operationState = OWSOperationStateExecuting; - [self didChangeValueForKey:OWSOperationKeyIsExecuting]; - - [self main]; -} - -- (void)markAsComplete -{ - [self willChangeValueForKey:OWSOperationKeyIsExecuting]; - [self willChangeValueForKey:OWSOperationKeyIsFinished]; - - // Ensure we call the success or failure handler exactly once. - @synchronized(self) - { - OWSAssertDebug(self.operationState != OWSOperationStateFinished); - - self.operationState = OWSOperationStateFinished; - } - - [self didChangeValueForKey:OWSOperationKeyIsExecuting]; - [self didChangeValueForKey:OWSOperationKeyIsFinished]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/ReachabilityManager.swift b/SignalUtilitiesKit/Utilities/ReachabilityManager.swift index c9b884db8..6f683bcb7 100644 --- a/SignalUtilitiesKit/Utilities/ReachabilityManager.swift +++ b/SignalUtilitiesKit/Utilities/ReachabilityManager.swift @@ -3,6 +3,7 @@ import Foundation import Reachability import SignalCoreKit +import SessionMessagingKit /// **Warning:** The simulator doesn't detect reachability correctly so if you are seeing odd/incorrect reachability states double /// check on an actual device before trying to replace this implementation diff --git a/SignalUtilitiesKit/Utilities/SignalIOS.pb.swift b/SignalUtilitiesKit/Utilities/SignalIOS.pb.swift deleted file mode 100644 index 588c729ed..000000000 --- a/SignalUtilitiesKit/Utilities/SignalIOS.pb.swift +++ /dev/null @@ -1,318 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: SignalIOS.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -//* -// Copyright (C) 2014-2016 Open Whisper Systems -// -// Licensed according to the LICENSE file in this repository. - -/// iOS - since we use a modern proto-compiler, we must specify -/// the legacy proto format. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct IOSProtos_BackupSnapshot { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var entity: [IOSProtos_BackupSnapshot.BackupEntity] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - struct BackupEntity { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// @required - var type: IOSProtos_BackupSnapshot.BackupEntity.TypeEnum { - get {return _type ?? .unknown} - set {_type = newValue} - } - /// Returns true if `type` has been explicitly set. - var hasType: Bool {return self._type != nil} - /// Clears the value of `type`. Subsequent reads from it will return its default value. - mutating func clearType() {self._type = nil} - - /// @required - var entityData: Data { - get {return _entityData ?? SwiftProtobuf.Internal.emptyData} - set {_entityData = newValue} - } - /// Returns true if `entityData` has been explicitly set. - var hasEntityData: Bool {return self._entityData != nil} - /// Clears the value of `entityData`. Subsequent reads from it will return its default value. - mutating func clearEntityData() {self._entityData = nil} - - /// @required - var collection: String { - get {return _collection ?? String()} - set {_collection = newValue} - } - /// Returns true if `collection` has been explicitly set. - var hasCollection: Bool {return self._collection != nil} - /// Clears the value of `collection`. Subsequent reads from it will return its default value. - mutating func clearCollection() {self._collection = nil} - - /// @required - var key: String { - get {return _key ?? String()} - set {_key = newValue} - } - /// Returns true if `key` has been explicitly set. - var hasKey: Bool {return self._key != nil} - /// Clears the value of `key`. Subsequent reads from it will return its default value. - mutating func clearKey() {self._key = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case unknown // = 0 - case migration // = 1 - case thread // = 2 - case interaction // = 3 - case attachment // = 4 - case misc // = 5 - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .migration - case 2: self = .thread - case 3: self = .interaction - case 4: self = .attachment - case 5: self = .misc - default: return nil - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .migration: return 1 - case .thread: return 2 - case .interaction: return 3 - case .attachment: return 4 - case .misc: return 5 - } - } - - } - - init() {} - - fileprivate var _type: IOSProtos_BackupSnapshot.BackupEntity.TypeEnum? = nil - fileprivate var _entityData: Data? = nil - fileprivate var _collection: String? = nil - fileprivate var _key: String? = nil - } - - init() {} -} - -#if swift(>=4.2) - -extension IOSProtos_BackupSnapshot.BackupEntity.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct IOSProtos_DeviceName { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// @required - var ephemeralPublic: Data { - get {return _ephemeralPublic ?? SwiftProtobuf.Internal.emptyData} - set {_ephemeralPublic = newValue} - } - /// Returns true if `ephemeralPublic` has been explicitly set. - var hasEphemeralPublic: Bool {return self._ephemeralPublic != nil} - /// Clears the value of `ephemeralPublic`. Subsequent reads from it will return its default value. - mutating func clearEphemeralPublic() {self._ephemeralPublic = nil} - - /// @required - var syntheticIv: Data { - get {return _syntheticIv ?? SwiftProtobuf.Internal.emptyData} - set {_syntheticIv = newValue} - } - /// Returns true if `syntheticIv` has been explicitly set. - var hasSyntheticIv: Bool {return self._syntheticIv != nil} - /// Clears the value of `syntheticIv`. Subsequent reads from it will return its default value. - mutating func clearSyntheticIv() {self._syntheticIv = nil} - - /// @required - var ciphertext: Data { - get {return _ciphertext ?? SwiftProtobuf.Internal.emptyData} - set {_ciphertext = newValue} - } - /// Returns true if `ciphertext` has been explicitly set. - var hasCiphertext: Bool {return self._ciphertext != nil} - /// Clears the value of `ciphertext`. Subsequent reads from it will return its default value. - mutating func clearCiphertext() {self._ciphertext = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _ephemeralPublic: Data? = nil - fileprivate var _syntheticIv: Data? = nil - fileprivate var _ciphertext: Data? = nil -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "IOSProtos" - -extension IOSProtos_BackupSnapshot: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".BackupSnapshot" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "entity"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeRepeatedMessageField(value: &self.entity) - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.entity.isEmpty { - try visitor.visitRepeatedMessageField(value: self.entity, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: IOSProtos_BackupSnapshot, rhs: IOSProtos_BackupSnapshot) -> Bool { - if lhs.entity != rhs.entity {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension IOSProtos_BackupSnapshot.BackupEntity: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = IOSProtos_BackupSnapshot.protoMessageName + ".BackupEntity" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "entityData"), - 3: .same(proto: "collection"), - 4: .same(proto: "key"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularEnumField(value: &self._type) - case 2: try decoder.decodeSingularBytesField(value: &self._entityData) - case 3: try decoder.decodeSingularStringField(value: &self._collection) - case 4: try decoder.decodeSingularStringField(value: &self._key) - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if let v = self._type { - try visitor.visitSingularEnumField(value: v, fieldNumber: 1) - } - if let v = self._entityData { - try visitor.visitSingularBytesField(value: v, fieldNumber: 2) - } - if let v = self._collection { - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - } - if let v = self._key { - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: IOSProtos_BackupSnapshot.BackupEntity, rhs: IOSProtos_BackupSnapshot.BackupEntity) -> Bool { - if lhs._type != rhs._type {return false} - if lhs._entityData != rhs._entityData {return false} - if lhs._collection != rhs._collection {return false} - if lhs._key != rhs._key {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension IOSProtos_BackupSnapshot.BackupEntity.TypeEnum: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "MIGRATION"), - 2: .same(proto: "THREAD"), - 3: .same(proto: "INTERACTION"), - 4: .same(proto: "ATTACHMENT"), - 5: .same(proto: "MISC"), - ] -} - -extension IOSProtos_DeviceName: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".DeviceName" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "ephemeralPublic"), - 2: .same(proto: "syntheticIv"), - 3: .same(proto: "ciphertext"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularBytesField(value: &self._ephemeralPublic) - case 2: try decoder.decodeSingularBytesField(value: &self._syntheticIv) - case 3: try decoder.decodeSingularBytesField(value: &self._ciphertext) - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if let v = self._ephemeralPublic { - try visitor.visitSingularBytesField(value: v, fieldNumber: 1) - } - if let v = self._syntheticIv { - try visitor.visitSingularBytesField(value: v, fieldNumber: 2) - } - if let v = self._ciphertext { - try visitor.visitSingularBytesField(value: v, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: IOSProtos_DeviceName, rhs: IOSProtos_DeviceName) -> Bool { - if lhs._ephemeralPublic != rhs._ephemeralPublic {return false} - if lhs._syntheticIv != rhs._syntheticIv {return false} - if lhs._ciphertext != rhs._ciphertext {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/SignalUtilitiesKit/Utilities/SignalIOSProto.swift b/SignalUtilitiesKit/Utilities/SignalIOSProto.swift deleted file mode 100644 index 5761fbda7..000000000 --- a/SignalUtilitiesKit/Utilities/SignalIOSProto.swift +++ /dev/null @@ -1,409 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -import Foundation - -// WARNING: This code is generated. Only edit within the markers. - -public enum SignalIOSProtoError: Error { - case invalidProtobuf(description: String) -} - -// MARK: - SignalIOSProtoBackupSnapshotBackupEntity - -@objc public class SignalIOSProtoBackupSnapshotBackupEntity: NSObject { - - // MARK: - SignalIOSProtoBackupSnapshotBackupEntityType - - @objc public enum SignalIOSProtoBackupSnapshotBackupEntityType: Int32 { - case unknown = 0 - case migration = 1 - case thread = 2 - case interaction = 3 - case attachment = 4 - case misc = 5 - } - - private class func SignalIOSProtoBackupSnapshotBackupEntityTypeWrap(_ value: IOSProtos_BackupSnapshot.BackupEntity.TypeEnum) -> SignalIOSProtoBackupSnapshotBackupEntityType { - switch value { - case .unknown: return .unknown - case .migration: return .migration - case .thread: return .thread - case .interaction: return .interaction - case .attachment: return .attachment - case .misc: return .misc - } - } - - private class func SignalIOSProtoBackupSnapshotBackupEntityTypeUnwrap(_ value: SignalIOSProtoBackupSnapshotBackupEntityType) -> IOSProtos_BackupSnapshot.BackupEntity.TypeEnum { - switch value { - case .unknown: return .unknown - case .migration: return .migration - case .thread: return .thread - case .interaction: return .interaction - case .attachment: return .attachment - case .misc: return .misc - } - } - - // MARK: - SignalIOSProtoBackupSnapshotBackupEntityBuilder - - @objc public class func builder(type: SignalIOSProtoBackupSnapshotBackupEntityType, entityData: Data, collection: String, key: String) -> SignalIOSProtoBackupSnapshotBackupEntityBuilder { - return SignalIOSProtoBackupSnapshotBackupEntityBuilder(type: type, entityData: entityData, collection: collection, key: key) - } - - // asBuilder() constructs a builder that reflects the proto's contents. - @objc public func asBuilder() -> SignalIOSProtoBackupSnapshotBackupEntityBuilder { - let builder = SignalIOSProtoBackupSnapshotBackupEntityBuilder(type: type, entityData: entityData, collection: collection, key: key) - return builder - } - - @objc public class SignalIOSProtoBackupSnapshotBackupEntityBuilder: NSObject { - - private var proto = IOSProtos_BackupSnapshot.BackupEntity() - - @objc fileprivate override init() {} - - @objc fileprivate init(type: SignalIOSProtoBackupSnapshotBackupEntityType, entityData: Data, collection: String, key: String) { - super.init() - - setType(type) - setEntityData(entityData) - setCollection(collection) - setKey(key) - } - - @objc public func setType(_ valueParam: SignalIOSProtoBackupSnapshotBackupEntityType) { - proto.type = SignalIOSProtoBackupSnapshotBackupEntityTypeUnwrap(valueParam) - } - - @objc public func setEntityData(_ valueParam: Data) { - proto.entityData = valueParam - } - - @objc public func setCollection(_ valueParam: String) { - proto.collection = valueParam - } - - @objc public func setKey(_ valueParam: String) { - proto.key = valueParam - } - - @objc public func build() throws -> SignalIOSProtoBackupSnapshotBackupEntity { - return try SignalIOSProtoBackupSnapshotBackupEntity.parseProto(proto) - } - - @objc public func buildSerializedData() throws -> Data { - return try SignalIOSProtoBackupSnapshotBackupEntity.parseProto(proto).serializedData() - } - } - - fileprivate let proto: IOSProtos_BackupSnapshot.BackupEntity - - @objc public let type: SignalIOSProtoBackupSnapshotBackupEntityType - - @objc public let entityData: Data - - @objc public let collection: String - - @objc public let key: String - - private init(proto: IOSProtos_BackupSnapshot.BackupEntity, - type: SignalIOSProtoBackupSnapshotBackupEntityType, - entityData: Data, - collection: String, - key: String) { - self.proto = proto - self.type = type - self.entityData = entityData - self.collection = collection - self.key = key - } - - @objc - public func serializedData() throws -> Data { - return try self.proto.serializedData() - } - - @objc public class func parseData(_ serializedData: Data) throws -> SignalIOSProtoBackupSnapshotBackupEntity { - let proto = try IOSProtos_BackupSnapshot.BackupEntity(serializedData: serializedData) - return try parseProto(proto) - } - - fileprivate class func parseProto(_ proto: IOSProtos_BackupSnapshot.BackupEntity) throws -> SignalIOSProtoBackupSnapshotBackupEntity { - guard proto.hasType else { - throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: type") - } - let type = SignalIOSProtoBackupSnapshotBackupEntityTypeWrap(proto.type) - - guard proto.hasEntityData else { - throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: entityData") - } - let entityData = proto.entityData - - guard proto.hasCollection else { - throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: collection") - } - let collection = proto.collection - - guard proto.hasKey else { - throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: key") - } - let key = proto.key - - // MARK: - Begin Validation Logic for SignalIOSProtoBackupSnapshotBackupEntity - - - // MARK: - End Validation Logic for SignalIOSProtoBackupSnapshotBackupEntity - - - let result = SignalIOSProtoBackupSnapshotBackupEntity(proto: proto, - type: type, - entityData: entityData, - collection: collection, - key: key) - return result - } - - @objc public override var debugDescription: String { - return "\(proto)" - } -} - -#if DEBUG - -extension SignalIOSProtoBackupSnapshotBackupEntity { - @objc public func serializedDataIgnoringErrors() -> Data? { - return try! self.serializedData() - } -} - -extension SignalIOSProtoBackupSnapshotBackupEntity.SignalIOSProtoBackupSnapshotBackupEntityBuilder { - @objc public func buildIgnoringErrors() -> SignalIOSProtoBackupSnapshotBackupEntity? { - return try! self.build() - } -} - -#endif - -// MARK: - SignalIOSProtoBackupSnapshot - -@objc public class SignalIOSProtoBackupSnapshot: NSObject { - - // MARK: - SignalIOSProtoBackupSnapshotBuilder - - @objc public class func builder() -> SignalIOSProtoBackupSnapshotBuilder { - return SignalIOSProtoBackupSnapshotBuilder() - } - - // asBuilder() constructs a builder that reflects the proto's contents. - @objc public func asBuilder() -> SignalIOSProtoBackupSnapshotBuilder { - let builder = SignalIOSProtoBackupSnapshotBuilder() - builder.setEntity(entity) - return builder - } - - @objc public class SignalIOSProtoBackupSnapshotBuilder: NSObject { - - private var proto = IOSProtos_BackupSnapshot() - - @objc fileprivate override init() {} - - @objc public func addEntity(_ valueParam: SignalIOSProtoBackupSnapshotBackupEntity) { - var items = proto.entity - items.append(valueParam.proto) - proto.entity = items - } - - @objc public func setEntity(_ wrappedItems: [SignalIOSProtoBackupSnapshotBackupEntity]) { - proto.entity = wrappedItems.map { $0.proto } - } - - @objc public func build() throws -> SignalIOSProtoBackupSnapshot { - return try SignalIOSProtoBackupSnapshot.parseProto(proto) - } - - @objc public func buildSerializedData() throws -> Data { - return try SignalIOSProtoBackupSnapshot.parseProto(proto).serializedData() - } - } - - fileprivate let proto: IOSProtos_BackupSnapshot - - @objc public let entity: [SignalIOSProtoBackupSnapshotBackupEntity] - - private init(proto: IOSProtos_BackupSnapshot, - entity: [SignalIOSProtoBackupSnapshotBackupEntity]) { - self.proto = proto - self.entity = entity - } - - @objc - public func serializedData() throws -> Data { - return try self.proto.serializedData() - } - - @objc public class func parseData(_ serializedData: Data) throws -> SignalIOSProtoBackupSnapshot { - let proto = try IOSProtos_BackupSnapshot(serializedData: serializedData) - return try parseProto(proto) - } - - fileprivate class func parseProto(_ proto: IOSProtos_BackupSnapshot) throws -> SignalIOSProtoBackupSnapshot { - var entity: [SignalIOSProtoBackupSnapshotBackupEntity] = [] - entity = try proto.entity.map { try SignalIOSProtoBackupSnapshotBackupEntity.parseProto($0) } - - // MARK: - Begin Validation Logic for SignalIOSProtoBackupSnapshot - - - // MARK: - End Validation Logic for SignalIOSProtoBackupSnapshot - - - let result = SignalIOSProtoBackupSnapshot(proto: proto, - entity: entity) - return result - } - - @objc public override var debugDescription: String { - return "\(proto)" - } -} - -#if DEBUG - -extension SignalIOSProtoBackupSnapshot { - @objc public func serializedDataIgnoringErrors() -> Data? { - return try! self.serializedData() - } -} - -extension SignalIOSProtoBackupSnapshot.SignalIOSProtoBackupSnapshotBuilder { - @objc public func buildIgnoringErrors() -> SignalIOSProtoBackupSnapshot? { - return try! self.build() - } -} - -#endif - -// MARK: - SignalIOSProtoDeviceName - -@objc public class SignalIOSProtoDeviceName: NSObject { - - // MARK: - SignalIOSProtoDeviceNameBuilder - - @objc public class func builder(ephemeralPublic: Data, syntheticIv: Data, ciphertext: Data) -> SignalIOSProtoDeviceNameBuilder { - return SignalIOSProtoDeviceNameBuilder(ephemeralPublic: ephemeralPublic, syntheticIv: syntheticIv, ciphertext: ciphertext) - } - - // asBuilder() constructs a builder that reflects the proto's contents. - @objc public func asBuilder() -> SignalIOSProtoDeviceNameBuilder { - let builder = SignalIOSProtoDeviceNameBuilder(ephemeralPublic: ephemeralPublic, syntheticIv: syntheticIv, ciphertext: ciphertext) - return builder - } - - @objc public class SignalIOSProtoDeviceNameBuilder: NSObject { - - private var proto = IOSProtos_DeviceName() - - @objc fileprivate override init() {} - - @objc fileprivate init(ephemeralPublic: Data, syntheticIv: Data, ciphertext: Data) { - super.init() - - setEphemeralPublic(ephemeralPublic) - setSyntheticIv(syntheticIv) - setCiphertext(ciphertext) - } - - @objc public func setEphemeralPublic(_ valueParam: Data) { - proto.ephemeralPublic = valueParam - } - - @objc public func setSyntheticIv(_ valueParam: Data) { - proto.syntheticIv = valueParam - } - - @objc public func setCiphertext(_ valueParam: Data) { - proto.ciphertext = valueParam - } - - @objc public func build() throws -> SignalIOSProtoDeviceName { - return try SignalIOSProtoDeviceName.parseProto(proto) - } - - @objc public func buildSerializedData() throws -> Data { - return try SignalIOSProtoDeviceName.parseProto(proto).serializedData() - } - } - - fileprivate let proto: IOSProtos_DeviceName - - @objc public let ephemeralPublic: Data - - @objc public let syntheticIv: Data - - @objc public let ciphertext: Data - - private init(proto: IOSProtos_DeviceName, - ephemeralPublic: Data, - syntheticIv: Data, - ciphertext: Data) { - self.proto = proto - self.ephemeralPublic = ephemeralPublic - self.syntheticIv = syntheticIv - self.ciphertext = ciphertext - } - - @objc - public func serializedData() throws -> Data { - return try self.proto.serializedData() - } - - @objc public class func parseData(_ serializedData: Data) throws -> SignalIOSProtoDeviceName { - let proto = try IOSProtos_DeviceName(serializedData: serializedData) - return try parseProto(proto) - } - - fileprivate class func parseProto(_ proto: IOSProtos_DeviceName) throws -> SignalIOSProtoDeviceName { - guard proto.hasEphemeralPublic else { - throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: ephemeralPublic") - } - let ephemeralPublic = proto.ephemeralPublic - - guard proto.hasSyntheticIv else { - throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: syntheticIv") - } - let syntheticIv = proto.syntheticIv - - guard proto.hasCiphertext else { - throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: ciphertext") - } - let ciphertext = proto.ciphertext - - // MARK: - Begin Validation Logic for SignalIOSProtoDeviceName - - - // MARK: - End Validation Logic for SignalIOSProtoDeviceName - - - let result = SignalIOSProtoDeviceName(proto: proto, - ephemeralPublic: ephemeralPublic, - syntheticIv: syntheticIv, - ciphertext: ciphertext) - return result - } - - @objc public override var debugDescription: String { - return "\(proto)" - } -} - -#if DEBUG - -extension SignalIOSProtoDeviceName { - @objc public func serializedDataIgnoringErrors() -> Data? { - return try! self.serializedData() - } -} - -extension SignalIOSProtoDeviceName.SignalIOSProtoDeviceNameBuilder { - @objc public func buildIgnoringErrors() -> SignalIOSProtoDeviceName? { - return try! self.build() - } -} - -#endif diff --git a/SignalUtilitiesKit/Utilities/SwiftSingletons.swift b/SignalUtilitiesKit/Utilities/SwiftSingletons.swift index 5af0a3177..40bee1b66 100644 --- a/SignalUtilitiesKit/Utilities/SwiftSingletons.swift +++ b/SignalUtilitiesKit/Utilities/SwiftSingletons.swift @@ -2,6 +2,7 @@ import Foundation import SignalCoreKit +import SessionUtilitiesKit public class SwiftSingletons: NSObject { public static let shared = SwiftSingletons() diff --git a/SignalUtilitiesKit/Utilities/TSConstants.h b/SignalUtilitiesKit/Utilities/TSConstants.h deleted file mode 100644 index 3d8542585..000000000 --- a/SignalUtilitiesKit/Utilities/TSConstants.h +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -#ifndef TextSecureKit_Constants_h -#define TextSecureKit_Constants_h - -extern const NSUInteger kOversizeTextMessageSizeThreshold; - -typedef NS_ENUM(NSInteger, TSWhisperMessageType) { - TSUnknownMessageType = 0, - TSEncryptedWhisperMessageType = 1, - TSIgnoreOnIOSWhisperMessageType = 2, // on droid this is the prekey bundle message irrelevant for us - TSPreKeyWhisperMessageType = 3, - TSUnencryptedWhisperMessageType = 4, - TSUnidentifiedSenderMessageType = 6, - TSClosedGroupCiphertextMessageType = 7, - TSFallbackMessageType = 101 -}; - -#pragma mark Server Address - -#define textSecureHTTPTimeOut 10 - -#define kLegalTermsUrlString @"https://getsession.org/privacy-policy/" - -//#ifndef DEBUG - -// Production -#define textSecureWebSocketAPI @"wss://textsecure-service.whispersystems.org/v1/websocket/" -#define textSecureCDNServerURL @"https://cdn.signal.org" -// Use same reflector for service and CDN -#define textSecureServiceReflectorHost @"europe-west1-signal-cdn-reflector.cloudfunctions.net" -#define textSecureCDNReflectorHost @"europe-west1-signal-cdn-reflector.cloudfunctions.net" -#define contactDiscoveryURL @"https://api.directory.signal.org" -#define kUDTrustRoot @"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF" -#define USING_PRODUCTION_SERVICE - -//#else - -// Staging -//#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/" -//#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/" -//#define textSecureCDNServerURL @"https://cdn-staging.signal.org" -//#define textSecureServiceReflectorHost @"meek-signal-service-staging.appspot.com"; -//#define textSecureCDNReflectorHost @"meek-signal-cdn-staging.appspot.com"; -//#define contactDiscoveryURL @"https://api-staging.directory.signal.org" -//#define kUDTrustRoot @"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx" - -//#endif - -BOOL IsUsingProductionService(void); - -#define textSecureAccountsAPI @"v1/accounts" -#define textSecureAttributesAPI @"/attributes/" - -#define textSecureMessagesAPI @"v1/messages/" -#define textSecureKeysAPI @"v2/keys" -#define textSecureSignedKeysAPI @"v2/keys/signed" -#define textSecureDirectoryAPI @"v1/directory" -#define textSecureAttachmentsAPI @"v1/attachments" -#define textSecureDeviceProvisioningCodeAPI @"v1/devices/provisioning/code" -#define textSecureDeviceProvisioningAPIFormat @"v1/provisioning/%@" -#define textSecureDevicesAPIFormat @"v1/devices/%@" -#define textSecureProfileAPIFormat @"v1/profile/%@" -#define textSecureSetProfileNameAPIFormat @"v1/profile/name/%@" -#define textSecureProfileAvatarFormAPI @"v1/profile/form/avatar" -#define textSecure2FAAPI @"/v1/accounts/pin" - -#define SignalApplicationGroup @"group.com.loki-project.loki-messenger" - -#endif - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/TSConstants.m b/SignalUtilitiesKit/Utilities/TSConstants.m deleted file mode 100644 index fbd6607e9..000000000 --- a/SignalUtilitiesKit/Utilities/TSConstants.m +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "TSConstants.h" - -NS_ASSUME_NONNULL_BEGIN - -const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; - -BOOL IsUsingProductionService() -{ -#ifdef USING_PRODUCTION_SERVICE - return YES; -#else - return NO; -#endif -} - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/UIView+OWS.swift b/SignalUtilitiesKit/Utilities/UIView+OWS.swift index de1a7c5cd..b158c207c 100644 --- a/SignalUtilitiesKit/Utilities/UIView+OWS.swift +++ b/SignalUtilitiesKit/Utilities/UIView+OWS.swift @@ -3,6 +3,7 @@ import Foundation import SessionUIKit import SignalCoreKit +import SessionUtilitiesKit public extension UIEdgeInsets { init(top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat) { diff --git a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift index 6791a15e1..d2904ab77 100644 --- a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift +++ b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift @@ -2,6 +2,7 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit public extension UIViewController { func findFrontmostViewController(ignoringAlerts: Bool) -> UIViewController {