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 cac7103f7..d720d5f32 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 */; }; @@ -128,7 +127,6 @@ 7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; - 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; }; 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */; }; 7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */; }; 7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */; }; @@ -314,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 */; }; @@ -531,6 +515,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 */; }; @@ -648,11 +635,12 @@ FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; }; FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; }; FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; }; + FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */; }; + FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */; }; FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; }; FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; }; FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; }; FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FC284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift */; }; - FD5C72FF284F0F120029977D /* MessageReceiver+ConfigurationMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FE284F0F120029977D /* MessageReceiver+ConfigurationMessages.swift */; }; FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C7300284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift */; }; FD5C7303284F0FA50029977D /* MessageReceiver+Calls.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C7302284F0FA50029977D /* MessageReceiver+Calls.swift */; }; FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C7304284F0FF30029977D /* MessageReceiver+VisibleMessages.swift */; }; @@ -662,6 +650,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 */; }; @@ -771,6 +760,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 */; }; @@ -788,7 +786,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 */; }; @@ -811,11 +809,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 */; }; @@ -825,6 +825,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 */; }; @@ -909,6 +910,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 */; }; @@ -1114,8 +1118,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 = ""; }; @@ -1237,7 +1239,6 @@ 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = ""; }; 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = ""; }; 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; - 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = ""; }; 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionListSheet.swift; sourceTree = ""; }; @@ -1418,9 +1419,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 = ""; }; @@ -1428,7 +1427,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 = ""; }; @@ -1436,7 +1434,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 = ""; }; @@ -1447,27 +1444,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 = ""; }; @@ -1689,6 +1676,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 = ""; }; @@ -1767,11 +1757,12 @@ FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = ""; }; FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = ""; }; + FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = ""; }; + FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = ""; }; FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = ""; }; FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = ""; }; FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = ""; }; FD5C72FC284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ExpirationTimers.swift"; sourceTree = ""; }; - FD5C72FE284F0F120029977D /* MessageReceiver+ConfigurationMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ConfigurationMessages.swift"; sourceTree = ""; }; FD5C7300284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+UnsendRequests.swift"; sourceTree = ""; }; FD5C7302284F0FA50029977D /* MessageReceiver+Calls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Calls.swift"; sourceTree = ""; }; FD5C7304284F0FF30029977D /* MessageReceiver+VisibleMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+VisibleMessages.swift"; sourceTree = ""; }; @@ -1782,6 +1773,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 = ""; }; @@ -1881,6 +1873,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 = ""; }; @@ -1897,7 +1898,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 = ""; }; @@ -1921,11 +1922,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 = ""; }; @@ -1935,6 +1938,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 = ""; }; @@ -2022,6 +2026,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 = ""; }; @@ -2730,7 +2737,6 @@ C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */, FD8ECF8A2935DB4B00C0D1BB /* SharedConfigMessage.swift */, C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */, - 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */, B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */, C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */, 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */, @@ -3126,6 +3132,7 @@ C379DC6825672B5E0002D4EB /* Notifications */ = { isa = PBXGroup; children = ( + FDC13D4E2A16EE41007267C7 /* Types */, FDC4382D27B383A600C60D73 /* Models */, FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */, C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */, @@ -3368,34 +3375,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 */, @@ -3425,8 +3418,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 */, @@ -3553,6 +3545,7 @@ FD09796527F6B0A800936362 /* Utilities */ = { isa = PBXGroup; children = ( + FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */, @@ -3618,6 +3611,7 @@ FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */, FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, + FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */, ); path = Migrations; sourceTree = ""; @@ -3683,6 +3677,7 @@ children = ( FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */, FD17D7B727F51ECA00122BE0 /* Migration.swift */, + FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */, FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */, FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */, @@ -3702,6 +3697,8 @@ FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */, + FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */, + FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */, ); path = Utilities; sourceTree = ""; @@ -3763,6 +3760,7 @@ FD2B4B022949886900AB4848 /* Database */ = { isa = PBXGroup; children = ( + FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */, FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */, ); path = Database; @@ -4002,7 +4000,6 @@ FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */, FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */, FD5C72FC284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift */, - FD5C72FE284F0F120029977D /* MessageReceiver+ConfigurationMessages.swift */, FD5C7300284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift */, FD5C7302284F0FA50029977D /* MessageReceiver+Calls.swift */, FD5C7304284F0FF30029977D /* MessageReceiver+VisibleMessages.swift */, @@ -4019,6 +4016,7 @@ FD83B9B927CF20A5005E1583 /* General */, FDDF074829DAB35200E5E8B5 /* JobRunner */, FD9B30F1293EA0AF008DEE3E /* Networking */, + FDFBB7522A2023DE00CA7350 /* Utilities */, FD29598E2A43BE5400888A17 /* Utilities */, ); path = SessionUtilitiesKitTests; @@ -4157,6 +4155,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 = ( @@ -4207,7 +4215,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 = ""; @@ -4407,6 +4425,14 @@ path = Models; sourceTree = ""; }; + FDFBB7522A2023DE00CA7350 /* Utilities */ = { + isa = PBXGroup; + children = ( + FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */, + ); + path = Utilities; + sourceTree = ""; + }; FDFDE122282D04E30098B17F /* Transitions */ = { isa = PBXGroup; children = ( @@ -4433,12 +4459,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 */, @@ -4739,6 +4759,7 @@ D221A080169C9E5E00537ABF /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; DefaultBuildSystemTypeForWorkspace = Original; LastSwiftUpdateCheck = 1430; LastTestingUpgradeCheck = 0600; @@ -5500,10 +5521,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 */, @@ -5511,7 +5530,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 */, @@ -5520,7 +5538,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 */, @@ -5536,9 +5553,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 */, @@ -5550,12 +5565,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; }; @@ -5658,6 +5671,8 @@ C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */, C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, + FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */, + FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, @@ -5669,10 +5684,12 @@ C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */, + FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */, FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */, 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 */, @@ -5757,6 +5774,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 */, @@ -5776,6 +5794,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 */, @@ -5789,6 +5808,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 */, @@ -5803,6 +5823,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 */, @@ -5811,18 +5832,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 */, @@ -5848,23 +5873,24 @@ 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 */, FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */, FD245C652850665400B966DD /* ClosedGroupControlMessage.swift in Sources */, FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */, - 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */, FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, @@ -5874,7 +5900,6 @@ C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, FD716E682850318E00C96BF4 /* CallMode.swift in Sources */, FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, - FD5C72FF284F0F120029977D /* MessageReceiver+ConfigurationMessages.swift in Sources */, FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, @@ -5885,7 +5910,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 */, @@ -5909,17 +5936,20 @@ 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 */, @@ -5983,10 +6013,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 */, @@ -6172,6 +6202,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 */, @@ -6414,7 +6445,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)"; @@ -6438,7 +6469,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)"; @@ -6486,7 +6517,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; @@ -6515,7 +6546,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)"; @@ -6551,7 +6582,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)"; @@ -6574,7 +6605,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"; @@ -6625,7 +6656,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; @@ -6653,7 +6684,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"; @@ -7585,7 +7616,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)", @@ -7623,7 +7654,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"; @@ -7656,7 +7687,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)", @@ -7694,7 +7725,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/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index bda79ae51..ea86a406c 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -156,7 +156,7 @@ final class QuoteView: UIView { if attachment.isVisualMedia { attachment.thumbnail( size: .small, - success: { image, _ in + success: { [imageView] image, _ in guard Thread.isMainThread else { DispatchQueue.main.async { imageView.image = image @@ -234,8 +234,6 @@ final class QuoteView: UIView { } // Label stack view - let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace) - let isCurrentUser: Bool = [ currentUserPublicKey, currentUserBlinded15PublicKey, @@ -288,9 +286,8 @@ final class QuoteView: UIView { cancelButton.set(.height, to: cancelButtonSize) cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside) - addSubview(cancelButton) + mainStackView.addArrangedSubview(cancelButton) cancelButton.center(.vertical, in: self) - cancelButton.pin(.right, to: .right, of: self) } } 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/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index 7d4103a85..fdb8ca753 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -203,6 +203,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U ]) } catch { + // Don't log the 'interrupt' error as that's just the user typing too fast + if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT { + SNLog("[GlobalSearch] Failed to find results due to error: \(error)") + } + return .failure(error) } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 50a7b6151..21c87c49a 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -283,14 +283,6 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData // Start polling if needed (i.e. if the user just created or restored their Session ID) if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate { appDelegate.startPollersIfNeeded() - - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - if !SessionUtil.userConfigsEnabled { - // Do this only if we created a new Session ID, or if we already received the initial configuration message - if UserDefaults.standard[.hasSyncedInitialConfiguration] { - appDelegate.syncConfigurationIfNeeded() - } - } } // Onion request path countries cache 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/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 10eff35e3..0ff182537 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -199,16 +199,18 @@ public class MediaGalleryViewModel { } } - public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable { - fileprivate static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) - fileprivate static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) - fileprivate static let interactionAuthorIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionAuthorId.stringValue) - fileprivate static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) - fileprivate static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - fileprivate static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) - fileprivate static let attachmentAlbumIndexKey: SQL = SQL(stringLiteral: CodingKeys.attachmentAlbumIndex.stringValue) - - fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue + public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case interactionId + case interactionVariant + case interactionAuthorId + case interactionTimestampMs + + case rowId + case attachmentAlbumIndex + case attachment + } public var id: String { attachment.id } public var differenceIdentifier: String { attachment.id } @@ -306,7 +308,7 @@ public class MediaGalleryViewModel { let finalFilterSQL: SQL = { guard let customFilters: SQL = customFilters else { return """ - WHERE \(attachment.alias[Column.rowID]) IN \(rowIds) + WHERE \(attachment[.rowId]) IN \(rowIds) """ } @@ -318,14 +320,14 @@ public class MediaGalleryViewModel { }() let request: SQLRequest = """ SELECT - \(interaction[.id]) AS \(Item.interactionIdKey), - \(interaction[.variant]) AS \(Item.interactionVariantKey), - \(interaction[.authorId]) AS \(Item.interactionAuthorIdKey), - \(interaction[.timestampMs]) AS \(Item.interactionTimestampMsKey), + \(interaction[.id]) AS \(Item.Columns.interactionId), + \(interaction[.variant]) AS \(Item.Columns.interactionVariant), + \(interaction[.authorId]) AS \(Item.Columns.interactionAuthorId), + \(interaction[.timestampMs]) AS \(Item.Columns.interactionTimestampMs), - \(attachment.alias[Column.rowID]) AS \(Item.rowIdKey), - \(interactionAttachment[.albumIndex]) AS \(Item.attachmentAlbumIndexKey), - \(Item.attachmentKey).* + \(attachment[.rowId]) AS \(Item.Columns.rowId), + \(interactionAttachment[.albumIndex]) AS \(Item.Columns.attachmentAlbumIndex), + \(attachment.allColumns) FROM \(Attachment.self) \(joinSQL) \(finalFilterSQL) @@ -338,8 +340,8 @@ public class MediaGalleryViewModel { Attachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - Item.attachmentString: adapters[1] + return ScopeAdapter.with(Item.self, [ + .attachment: adapters[1] ]) } } 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 a11d1f669..283baeaf5 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 { @@ -522,7 +523,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD startPollersIfNeeded() if CurrentAppContext().isMainApp { - syncConfigurationIfNeeded() handleAppActivatedWithOngoingCallIfNeeded() } } @@ -868,36 +868,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD presentingVC.present(callVC, animated: true, completion: nil) } - - // MARK: - Config Sync - - func syncConfigurationIfNeeded() { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard !SessionUtil.userConfigsEnabled else { return } - - let lastSync: Date = (UserDefaults.standard[.lastConfigurationSync] ?? .distantPast) - - guard Date().timeIntervalSince(lastSync) > (7 * 24 * 60 * 60) else { return } // Sync every 2 days - - Storage.shared - .writeAsync( - updates: { db in - ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db)) - }, - completion: { _, result in - switch result { - case .failure: break - case .success: - // Only update the 'lastConfigurationSync' timestamp if we have done the - // first sync (Don't want a new device config sync to override config - // syncs from other devices) - if UserDefaults.standard[.hasSyncedInitialConfiguration] { - UserDefaults.standard[.lastConfigurationSync] = Date() - } - } - } - ) - } } // MARK: - LifecycleMethod 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 c3fda3b34..8bcb25dae 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 d40fa50bd..73a0a30ef 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 8e83718e1..a6443d9b5 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 7f48d7e42..93eb19aee 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -483,6 +483,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" = "رسیدهای خواندن در چت‌های یک به یک روان شود."; @@ -645,6 +648,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 9167e07b6..12b1118c9 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 6b78436ee..13368d020 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 4ceb134f9..c129fe04d 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 1e9412ce3..f0165ad1c 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 12013921b..f2f2d802c 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 f301b4c65..65ea7de30 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 e141682fa..e8d9a1ec8 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 6dbbda2b3..6a76cd068 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 7613d5d0c..2c70b58fe 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 0dc850760..9c70d1638 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 33d5a4288..70d23dffd 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 8804247ad..ef9f0e74d 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 69eefbe68..619ad474c 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 c7d37b567..944c0917c 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 75612d387..576744efe 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 bc19cad24..9e7dab1f1 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 e80f39d42..cea8a4480 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 cb780247e..736085a01 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -483,6 +483,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."; @@ -645,6 +648,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 123b2fd33..7aab0b08d 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -30,13 +30,6 @@ enum Onboarding { _ requestId: UUID, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled else { - return Just(nil) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - let userPublicKey: String = getUserHexEncodedPublicKey() return SnodeAPI.getSwarm(for: userPublicKey) @@ -258,9 +251,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..0a612d0b0 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 @@ -257,11 +258,12 @@ class BlockedContactsViewModel: SessionTableViewModel = """ SELECT - \(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey), - \(DataModel.profileKey).* + \(profile[.rowId]) AS \(DataModel.Columns.rowId), + \(profile.allColumns) FROM \(Profile.self) - WHERE \(profile.alias[Column.rowID]) IN \(rowIds) + WHERE \(profile[.rowId]) IN \(rowIds) ORDER BY \(orderSQL) """ @@ -299,8 +301,8 @@ class BlockedContactsViewModel: 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 0aa4f5d37..2644c73ec 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -31,14 +31,9 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API _011_AddPendingReadReceipts.self, _012_AddFTSIfNeeded.self, _013_SessionUtilChanges.self, - // Wait until the feature is turned on before doing the migration that generates - // the config dump data - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - (Features.useSharedUtilForUserConfig(db) ? - _014_GenerateInitialUserConfigDumps.self : - (nil as Migration.Type?) - ) - ].compactMap { $0 } + _014_GenerateInitialUserConfigDumps.self, + _015_BlockCommunityMessageRequests.self + ] ] ) } diff --git a/SessionMessagingKit/Database/LegacyDatabase/SMKLegacy.swift b/SessionMessagingKit/Database/LegacyDatabase/SMKLegacy.swift index 7967974ac..f950fde8f 100644 --- a/SessionMessagingKit/Database/LegacyDatabase/SMKLegacy.swift +++ b/SessionMessagingKit/Database/LegacyDatabase/SMKLegacy.swift @@ -649,23 +649,18 @@ public enum SMKLegacy { @objc(SNConfigurationMessage) internal final class _ConfigurationMessage: _ControlMessage { - internal var closedGroups: Set<_CMClosedGroup> = [] - internal var openGroups: Set = [] internal var displayName: String? internal var profilePictureURL: String? internal var profileKey: Data? - internal var contacts: Set<_CMContact> = [] // MARK: NSCoding public required init?(coder: NSCoder) { super.init(coder: coder) - if let closedGroups = coder.decodeObject(forKey: "closedGroups") as! Set<_CMClosedGroup>? { self.closedGroups = closedGroups } - if let openGroups = coder.decodeObject(forKey: "openGroups") as! Set? { self.openGroups = openGroups } + if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName } if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL } if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey } - if let contacts = coder.decodeObject(forKey: "contacts") as! Set<_CMContact>? { self.contacts = contacts } } public override func encode(with coder: NSCoder) { @@ -679,126 +674,12 @@ public enum SMKLegacy { ConfigurationMessage( displayName: displayName, profilePictureUrl: profilePictureURL, - profileKey: profileKey, - closedGroups: closedGroups - .map { $0.toNonLegacy() } - .asSet(), - openGroups: openGroups, - contacts: contacts - .map { $0.toNonLegacy() } - .asSet() + profileKey: profileKey ) ) } } - // MARK: - Config Message Closed Group - - @objc(CMClosedGroup) - internal final class _CMClosedGroup: NSObject, NSCoding { - internal let publicKey: String - internal let name: String - internal let encryptionKeyPair: SUKLegacy.KeyPair - internal let members: Set - internal let admins: Set - internal let expirationTimer: UInt32 - - // MARK: NSCoding - - public required init?(coder: NSCoder) { - guard - let publicKey = coder.decodeObject(forKey: "publicKey") as! String?, - let name = coder.decodeObject(forKey: "name") as! String?, - let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as! SUKLegacy.KeyPair?, - let members = coder.decodeObject(forKey: "members") as! Set?, - let admins = coder.decodeObject(forKey: "admins") as! Set? - else { return nil } - - self.publicKey = publicKey - self.name = name - self.encryptionKeyPair = encryptionKeyPair - self.members = members - self.admins = admins - self.expirationTimer = (coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0) - } - - public func encode(with coder: NSCoder) { - fatalError("encode(with:) should never be called for legacy types") - } - - // MARK: Non-Legacy Conversion - - internal func toNonLegacy() -> ConfigurationMessage.CMClosedGroup { - return ConfigurationMessage.CMClosedGroup( - publicKey: publicKey, - name: name, - encryptionKeyPublicKey: encryptionKeyPair.publicKey, - encryptionKeySecretKey: encryptionKeyPair.privateKey, - members: members, - admins: admins, - expirationTimer: expirationTimer - ) - } - } - - // MARK: - Config Message Contact - - @objc(SNConfigurationMessageContact) - internal final class _CMContact: NSObject, NSCoding { - internal var publicKey: String? - internal var displayName: String? - internal var profilePictureURL: String? - internal var profileKey: Data? - - internal var hasIsApproved: Bool - internal var isApproved: Bool - internal var hasIsBlocked: Bool - internal var isBlocked: Bool - internal var hasDidApproveMe: Bool - internal var didApproveMe: Bool - - // MARK: NSCoding - - public required init?(coder: NSCoder) { - guard - let publicKey = coder.decodeObject(forKey: "publicKey") as! String?, - let displayName = coder.decodeObject(forKey: "displayName") as! String? - else { return nil } - - self.publicKey = publicKey - self.displayName = displayName - self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? - self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data? - self.hasIsApproved = (coder.decodeObject(forKey: "hasIsApproved") as? Bool ?? false) - self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false) - self.hasIsBlocked = (coder.decodeObject(forKey: "hasIsBlocked") as? Bool ?? false) - self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false) - self.hasDidApproveMe = (coder.decodeObject(forKey: "hasDidApproveMe") as? Bool ?? false) - self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false) - } - - public func encode(with coder: NSCoder) { - fatalError("encode(with:) should never be called for legacy types") - } - - // MARK: Non-Legacy Conversion - - internal func toNonLegacy() -> ConfigurationMessage.CMContact { - return ConfigurationMessage.CMContact( - publicKey: publicKey, - displayName: displayName, - profilePictureUrl: profilePictureURL, - profileKey: profileKey, - hasIsApproved: hasIsApproved, - isApproved: isApproved, - hasIsBlocked: hasIsBlocked, - isBlocked: isBlocked, - hasDidApproveMe: hasDidApproveMe, - didApproveMe: didApproveMe - ) - } - } - // MARK: - Unsend Request @objc(SNUnsendRequest) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 8918c1c9b..d7ac2eabf 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -422,7 +422,8 @@ enum _003_YDBToGRDBMigration: Migration { profilePictureUrl: legacyContact.profilePictureURL, profilePictureFileName: legacyContact.profilePictureFileName, profileEncryptionKey: legacyContact.profileEncryptionKey?.keyData, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + lastBlocksCommunityMessageRequests: 0 ).migrationSafeInsert(db) /// **Note:** The blow "shouldForce" flags are here to allow us to avoid having to run legacy migrations they @@ -645,7 +646,8 @@ enum _003_YDBToGRDBMigration: Migration { id: profileId, name: profileId, lastNameUpdate: 0, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + lastBlocksCommunityMessageRequests: 0 ).migrationSafeSave(db) } @@ -1059,7 +1061,8 @@ enum _003_YDBToGRDBMigration: Migration { id: quotedMessage.authorId, name: quotedMessage.authorId, lastNameUpdate: 0, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + lastBlocksCommunityMessageRequests: 0 ).migrationSafeSave(db) } @@ -1851,14 +1854,6 @@ enum _003_YDBToGRDBMigration: Migration { SMKLegacy._ConfigurationMessage.self, forClassName: "SNConfigurationMessage" ) - NSKeyedUnarchiver.setClass( - SMKLegacy._CMClosedGroup.self, - forClassName: "SNClosedGroup" - ) - NSKeyedUnarchiver.setClass( - SMKLegacy._CMContact.self, - forClassName: "SNConfigurationMessage.SNConfigurationMessageContact" - ) NSKeyedUnarchiver.setClass( SMKLegacy._UnsendRequest.self, forClassName: "SNUnsendRequest" 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/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift index 04b565056..442431a70 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift @@ -6,8 +6,6 @@ import SessionUtil import SessionUtilitiesKit /// This migration goes through the current state of the database and generates config dumps for the user config types -/// -/// **Note:** This migration won't be run until the `useSharedUtilForUserConfig` feature flag is enabled enum _014_GenerateInitialUserConfigDumps: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "GenerateInitialUserConfigDumps" 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/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index bedc9fc31..9c1733e6c 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -522,7 +522,7 @@ extension Attachment { \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) ) @@ -568,7 +568,7 @@ extension Attachment { \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) ) diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 43e922ed0..49ab5dfcf 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -95,6 +95,19 @@ public extension ClosedGroup { } } +// MARK: - Search Queries + +public extension ClosedGroup { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case name + } + + let name: String + } +} + // MARK: - Convenience public extension ClosedGroup { @@ -144,10 +157,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/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index c50645ca0..9d93212cd 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -29,13 +29,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu /// Whenever using this `linkPreview` association make sure to filter the result using /// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey) - public static var linkPreviewFilterLiteral: SQL = { - let interaction: TypedTableAlias = TypedTableAlias() - let linkPreview: TypedTableAlias = TypedTableAlias() + public static func linkPreviewFilterLiteral( + interaction: TypedTableAlias = TypedTableAlias(), + linkPreview: TypedTableAlias = TypedTableAlias() + ) -> SQL { let halfResolution: Double = LinkPreview.timstampResolution return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) * 1000 AND (\(linkPreview[.timestamp]) + \(halfResolution)) * 1000)" - }() + } public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey) public typealias Columns = CodingKeys @@ -695,6 +696,17 @@ public extension Interaction { // MARK: - Search Queries public extension Interaction { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case threadId + case body + } + + let threadId: String + let body: String + } + struct TimestampInfo: FetchableRecord, Codable { public let id: Int64 public let timestampMs: Int64 @@ -710,8 +722,7 @@ public extension Interaction { static func idsForTermWithin(threadId: String, pattern: FTS5Pattern) -> SQLRequest { let interaction: TypedTableAlias = TypedTableAlias() - let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) - let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name) + let interactionFullTextSearch: TypedTableAlias = TypedTableAlias(name: Interaction.fullTextSearchTableName) let request: SQLRequest = """ SELECT @@ -719,9 +730,9 @@ public extension Interaction { \(interaction[.timestampMs]) FROM \(Interaction.self) JOIN \(interactionFullTextSearch) ON ( - \(interactionFullTextSearch).rowid = \(interaction.alias[Column.rowID]) AND - \(SQL("\(interactionFullTextSearch).\(threadIdLiteral) = \(threadId)")) AND - \(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) + \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND + \(SQL("\(interactionFullTextSearch[.threadId]) = \(threadId)")) AND + \(interactionFullTextSearch[.body]) MATCH \(pattern) ) ORDER BY \(interaction[.timestampMs].desc) diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index 55da87c1d..d4b27a35c 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -215,6 +215,19 @@ public extension OpenGroup { } } +// MARK: - Search Queries + +public extension OpenGroup { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case name + } + + let name: String + } +} + // MARK: - Convenience public extension OpenGroup { diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index cc4e4bef6..7b8695929 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 ) } @@ -277,6 +298,21 @@ public extension Profile { } } +// MARK: - Search Queries + +public extension Profile { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case nickname + case name + } + + let nickname: String? + let name: String + } +} + // MARK: - Convenience public extension Profile { diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 163d38038..cb8f84fc3 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -365,7 +365,7 @@ public extension SessionThread { let contact: TypedTableAlias = TypedTableAlias() return """ - SELECT \(thread.allColumns()) + SELECT \(thread.allColumns) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) WHERE ( diff --git a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift index f19caf473..4b1d8011f 100644 --- a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift @@ -20,10 +20,7 @@ public enum ConfigurationSyncJob: JobExecutor { deferred: @escaping (Job, Dependencies) -> (), using dependencies: Dependencies ) { - guard - SessionUtil.userConfigsEnabled, - Identity.userCompletedRequiredOnboarding() - else { return success(job, true, dependencies) } + guard Identity.userCompletedRequiredOnboarding() else { return success(job, true, dependencies) } // It's possible for multiple ConfigSyncJob's with the same target (user/group) to try to run at the // same time since as soon as one is started we will enqueue a second one, rather than adding dependencies @@ -200,35 +197,6 @@ public extension ConfigurationSyncJob { publicKey: String, dependencies: Dependencies = Dependencies() ) { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled(db) else { - // If we don't have a userKeyPair (or name) yet then there is no need to sync the - // configuration as the user doesn't fully exist yet (this will get triggered on - // the first launch of a fresh install due to the migrations getting run and a few - // times during onboarding) - guard - Identity.userCompletedRequiredOnboarding(db), - let legacyConfigMessage: Message = try? ConfigurationMessage.getCurrent(db) - else { return } - - let publicKey: String = getUserHexEncodedPublicKey(db) - - dependencies.jobRunner.add( - db, - job: Job( - variant: .messageSend, - threadId: publicKey, - details: MessageSendJob.Details( - destination: Message.Destination.contact(publicKey: publicKey), - message: legacyConfigMessage - ) - ), - canStartJob: true, - using: dependencies - ) - return - } - // Upsert a config sync job if needed dependencies.jobRunner.upsert( db, @@ -268,30 +236,6 @@ public extension ConfigurationSyncJob { } static func run(using dependencies: Dependencies = Dependencies()) -> AnyPublisher { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled else { - return Storage.shared - .writePublisher { db -> MessageSender.PreparedSendData in - // If we don't have a userKeyPair yet then there is no need to sync the configuration - // as the user doesn't exist yet (this will get triggered on the first launch of a - // fresh install due to the migrations getting run) - guard Identity.userCompletedRequiredOnboarding(db) else { throw StorageError.generic } - - let publicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) - - return try MessageSender.preparedSendData( - db, - message: try ConfigurationMessage.getCurrent(db), - to: Message.Destination.contact(publicKey: publicKey), - namespace: .default, - interactionId: nil, - using: dependencies - ) - } - .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } - .eraseToAnyPublisher() - } - // Trigger the job emitting the result when completed return Deferred { Future { resolver in diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 7b5989579..633588a3d 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -82,7 +82,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Interaction.self) WHERE \(Column.rowID) IN ( - SELECT \(interaction.alias[Column.rowID]) + SELECT \(interaction[.rowId]) FROM \(Interaction.self) JOIN \(SessionThread.self) ON ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND @@ -90,7 +90,7 @@ public enum GarbageCollectionJob: JobExecutor { ) JOIN ( SELECT - COUNT(\(interaction.alias[Column.rowID])) AS interactionCount, + COUNT(\(interaction[.rowId])) AS interactionCount, \(interaction[.threadId]) FROM \(Interaction.self) GROUP BY \(interaction[.threadId]) @@ -112,7 +112,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Job.self) WHERE \(Column.rowID) IN ( - SELECT \(job.alias[Column.rowID]) + SELECT \(job[.rowId]) FROM \(Job.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(job[.threadId]) LEFT JOIN \(Interaction.self) ON \(interaction[.id]) = \(job[.interactionId]) @@ -139,11 +139,11 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(LinkPreview.self) WHERE \(Column.rowID) IN ( - SELECT \(linkPreview.alias[Column.rowID]) + SELECT \(linkPreview[.rowId]) FROM \(LinkPreview.self) LEFT JOIN \(Interaction.self) ON ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) WHERE \(interaction[.id]) IS NULL ) @@ -159,7 +159,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(OpenGroup.self) WHERE \(Column.rowID) IN ( - SELECT \(openGroup.alias[Column.rowID]) + SELECT \(openGroup[.rowId]) FROM \(OpenGroup.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId]) WHERE ( @@ -178,7 +178,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Capability.self) WHERE \(Column.rowID) IN ( - SELECT \(capability.alias[Column.rowID]) + SELECT \(capability[.rowId]) FROM \(Capability.self) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.server]) = \(capability[.openGroupServer]) WHERE \(openGroup[.threadId]) IS NULL @@ -195,7 +195,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(BlindedIdLookup.self) WHERE \(Column.rowID) IN ( - SELECT \(blindedIdLookup.alias[Column.rowID]) + SELECT \(blindedIdLookup[.rowId]) FROM \(BlindedIdLookup.self) LEFT JOIN \(SessionThread.self) ON ( \(thread[.id]) = \(blindedIdLookup[.blindedId]) OR @@ -222,7 +222,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Contact.self) WHERE \(Column.rowID) IN ( - SELECT \(contact.alias[Column.rowID]) + SELECT \(contact[.rowId]) FROM \(Contact.self) LEFT JOIN \(BlindedIdLookup.self) ON ( \(blindedIdLookup[.blindedId]) = \(contact[.id]) AND @@ -243,7 +243,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Attachment.self) WHERE \(Column.rowID) IN ( - SELECT \(attachment.alias[Column.rowID]) + SELECT \(attachment[.rowId]) FROM \(Attachment.self) LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id]) LEFT JOIN \(LinkPreview.self) ON \(linkPreview[.attachmentId]) = \(attachment[.id]) @@ -269,7 +269,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Profile.self) WHERE \(Column.rowID) IN ( - SELECT \(profile.alias[Column.rowID]) + SELECT \(profile[.rowId]) FROM \(Profile.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(profile[.id]) LEFT JOIN \(Interaction.self) ON \(interaction[.authorId]) = \(profile[.id]) @@ -310,7 +310,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(SessionThread.self) WHERE \(Column.rowID) IN ( - SELECT \(thread.alias[Column.rowID]) + SELECT \(thread[.rowId]) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) 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/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift deleted file mode 100644 index 6c8c5977a..000000000 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import SessionUtilitiesKit - -extension ConfigurationMessage { - public static func getCurrent(_ db: Database) throws -> ConfigurationMessage { - let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser(db) - let displayName: String = currentUserProfile.name - let profilePictureUrl: String? = currentUserProfile.profilePictureUrl - let profileKey: Data? = currentUserProfile.profileEncryptionKey - let closedGroups: Set = try ClosedGroup.fetchAll(db) - .compactMap { closedGroup -> CMClosedGroup? in - guard let latestKeyPair: ClosedGroupKeyPair = try closedGroup.fetchLatestKeyPair(db) else { - return nil - } - - return CMClosedGroup( - publicKey: closedGroup.publicKey, - name: closedGroup.name, - encryptionKeyPublicKey: latestKeyPair.publicKey, - encryptionKeySecretKey: latestKeyPair.secretKey, - members: try closedGroup.members - .select(GroupMember.Columns.profileId) - .asRequest(of: String.self) - .fetchSet(db), - admins: try closedGroup.admins - .select(GroupMember.Columns.profileId) - .asRequest(of: String.self) - .fetchSet(db), - expirationTimer: (try? DisappearingMessagesConfiguration - .fetchOne(db, id: closedGroup.threadId) - .map { ($0.isEnabled ? UInt32($0.durationSeconds) : 0) }) - .defaulting(to: 0) - ) - } - .asSet() - // The default room promise creates an OpenGroup with an empty `roomToken` value, - // we don't want to start a poller for this as the user hasn't actually joined a room - let openGroups: Set = try OpenGroup - .filter(OpenGroup.Columns.roomToken != "") - .filter(OpenGroup.Columns.isActive) - .fetchAll(db) - .compactMap { openGroup in - SessionUtil.communityUrlFor( - server: openGroup.server, - roomToken: openGroup.roomToken, - publicKey: openGroup.publicKey - ) - } - .asSet() - let contacts: Set = try Contact - .filter(Contact.Columns.id != currentUserProfile.id) - .fetchAll(db) - .map { contact -> CMContact in - // Can just default the 'hasX' values to true as they will be set to this - // when converting to proto anyway - let profile: Profile? = try? Profile.fetchOne(db, id: contact.id) - - return CMContact( - publicKey: contact.id, - displayName: (profile?.name ?? contact.id), - profilePictureUrl: profile?.profilePictureUrl, - profileKey: profile?.profileEncryptionKey, - hasIsApproved: true, - isApproved: contact.isApproved, - hasIsBlocked: true, - isBlocked: contact.isBlocked, - hasDidApproveMe: true, - didApproveMe: contact.didApproveMe - ) - } - .asSet() - - return ConfigurationMessage( - displayName: displayName, - profilePictureUrl: profilePictureUrl, - profileKey: profileKey, - closedGroups: closedGroups, - openGroups: openGroups, - contacts: contacts - ) - } -} diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift index 44977dd52..d161ef92e 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift @@ -6,361 +6,80 @@ import SessionUtilitiesKit public final class ConfigurationMessage: ControlMessage { private enum CodingKeys: String, CodingKey { - case closedGroups - case openGroups case displayName case profilePictureUrl case profileKey - case contacts } - public var closedGroups: Set = [] - public var openGroups: Set = [] public var displayName: String? public var profilePictureUrl: String? public var profileKey: Data? - public var contacts: Set = [] public override var isSelfSendValid: Bool { true } - + // MARK: - Initialization public init( displayName: String?, profilePictureUrl: String?, - profileKey: Data?, - closedGroups: Set, - openGroups: Set, - contacts: Set + profileKey: Data? ) { super.init() - + self.displayName = displayName self.profilePictureUrl = profilePictureUrl self.profileKey = profileKey - self.closedGroups = closedGroups - self.openGroups = openGroups - self.contacts = contacts } - + // MARK: - Codable - + required init(from decoder: Decoder) throws { try super.init(from: decoder) - + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - closedGroups = ((try? container.decode(Set.self, forKey: .closedGroups)) ?? []) - openGroups = ((try? container.decode(Set.self, forKey: .openGroups)) ?? []) + displayName = try? container.decode(String.self, forKey: .displayName) profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl) profileKey = try? container.decode(Data.self, forKey: .profileKey) - contacts = ((try? container.decode(Set.self, forKey: .contacts)) ?? []) } - + public override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) - + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(closedGroups, forKey: .closedGroups) - try container.encodeIfPresent(openGroups, forKey: .openGroups) + try container.encodeIfPresent(displayName, forKey: .displayName) try container.encodeIfPresent(profilePictureUrl, forKey: .profilePictureUrl) try container.encodeIfPresent(profileKey, forKey: .profileKey) - try container.encodeIfPresent(contacts, forKey: .contacts) } // MARK: - Proto Conversion public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ConfigurationMessage? { guard let configurationProto = proto.configurationMessage else { return nil } + let displayName = configurationProto.displayName let profilePictureUrl = configurationProto.profilePicture let profileKey = configurationProto.profileKey - let closedGroups = Set(configurationProto.closedGroups.compactMap { CMClosedGroup.fromProto($0) }) - let openGroups = Set(configurationProto.openGroups) - let contacts = Set(configurationProto.contacts.compactMap { CMContact.fromProto($0) }) - + return ConfigurationMessage( displayName: displayName, profilePictureUrl: profilePictureUrl, - profileKey: profileKey, - closedGroups: closedGroups, - openGroups: openGroups, - contacts: contacts + profileKey: profileKey ) } - public override func toProto(_ db: Database) -> SNProtoContent? { - let configurationProto = SNProtoConfigurationMessage.builder() - if let displayName = displayName { configurationProto.setDisplayName(displayName) } - if let profilePictureUrl = profilePictureUrl { configurationProto.setProfilePicture(profilePictureUrl) } - if let profileKey = profileKey { configurationProto.setProfileKey(profileKey) } - configurationProto.setClosedGroups(closedGroups.compactMap { $0.toProto() }) - configurationProto.setOpenGroups([String](openGroups)) - configurationProto.setContacts(contacts.compactMap { $0.toProto() }) - let contentProto = SNProtoContent.builder() - do { - contentProto.setConfigurationMessage(try configurationProto.build()) - return try contentProto.build() - } catch { - SNLog("Couldn't construct configuration proto from: \(self).") - return nil - } - } + public override func toProto(_ db: Database) -> SNProtoContent? { return nil } // MARK: - Description public var description: String { """ - ConfigurationMessage( - closedGroups: \([CMClosedGroup](closedGroups).prettifiedDescription), - openGroups: \([String](openGroups).prettifiedDescription), + LegacyConfigurationMessage( displayName: \(displayName ?? "null"), profilePictureUrl: \(profilePictureUrl ?? "null"), - profileKey: \(profileKey?.toHexString() ?? "null"), - contacts: \([CMContact](contacts).prettifiedDescription) + profileKey: \(profileKey?.toHexString() ?? "null") ) """ } } - -// MARK: - Closed Group - -extension ConfigurationMessage { - public struct CMClosedGroup: Codable, Hashable, CustomStringConvertible { - private enum CodingKeys: String, CodingKey { - case publicKey - case name - case encryptionKeyPublicKey - case encryptionKeySecretKey - case members - case admins - case expirationTimer - } - - public let publicKey: String - public let name: String - public let encryptionKeyPublicKey: Data - public let encryptionKeySecretKey: Data - public let members: Set - public let admins: Set - public let expirationTimer: UInt32 - - public var isValid: Bool { !members.isEmpty && !admins.isEmpty } - - // MARK: - Initialization - - public init( - publicKey: String, - name: String, - encryptionKeyPublicKey: Data, - encryptionKeySecretKey: Data, - members: Set, - admins: Set, - expirationTimer: UInt32 - ) { - self.publicKey = publicKey - self.name = name - self.encryptionKeyPublicKey = encryptionKeyPublicKey - self.encryptionKeySecretKey = encryptionKeySecretKey - self.members = members - self.admins = admins - self.expirationTimer = expirationTimer - } - - // MARK: - Codable - - public init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - publicKey = try container.decode(String.self, forKey: .publicKey) - name = try container.decode(String.self, forKey: .name) - encryptionKeyPublicKey = try container.decode(Data.self, forKey: .encryptionKeyPublicKey) - encryptionKeySecretKey = try container.decode(Data.self, forKey: .encryptionKeySecretKey) - members = try container.decode(Set.self, forKey: .members) - admins = try container.decode(Set.self, forKey: .admins) - expirationTimer = try container.decode(UInt32.self, forKey: .expirationTimer) - } - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(publicKey, forKey: .publicKey) - try container.encode(name, forKey: .name) - try container.encode(encryptionKeyPublicKey, forKey: .encryptionKeyPublicKey) - try container.encode(encryptionKeySecretKey, forKey: .encryptionKeySecretKey) - try container.encode(members, forKey: .members) - try container.encode(admins, forKey: .admins) - try container.encode(expirationTimer, forKey: .expirationTimer) - } - - public static func fromProto(_ proto: SNProtoConfigurationMessageClosedGroup) -> CMClosedGroup? { - guard - let publicKey = proto.publicKey?.toHexString(), - let name = proto.name, - let encryptionKeyPairAsProto = proto.encryptionKeyPair - else { return nil } - - let members = Set(proto.members.map { $0.toHexString() }) - let admins = Set(proto.admins.map { $0.toHexString() }) - let expirationTimer = proto.expirationTimer - let result = CMClosedGroup( - publicKey: publicKey, - name: name, - encryptionKeyPublicKey: encryptionKeyPairAsProto.publicKey, - encryptionKeySecretKey: encryptionKeyPairAsProto.privateKey, - members: members, - admins: admins, - expirationTimer: expirationTimer - ) - - guard result.isValid else { return nil } - return result - } - - public func toProto() -> SNProtoConfigurationMessageClosedGroup? { - guard isValid else { return nil } - let result = SNProtoConfigurationMessageClosedGroup.builder() - result.setPublicKey(Data(hex: publicKey)) - result.setName(name) - do { - let encryptionKeyPairAsProto = try SNProtoKeyPair.builder( - publicKey: encryptionKeyPublicKey, - privateKey: encryptionKeySecretKey - ).build() - result.setEncryptionKeyPair(encryptionKeyPairAsProto) - } catch { - SNLog("Couldn't construct closed group proto from: \(self).") - return nil - } - result.setMembers(members.map { Data(hex: $0) }) - result.setAdmins(admins.map { Data(hex: $0) }) - result.setExpirationTimer(expirationTimer) - do { - return try result.build() - } catch { - SNLog("Couldn't construct closed group proto from: \(self).") - return nil - } - } - - public var description: String { name } - } -} - -// MARK: - Contact - -extension ConfigurationMessage { - public struct CMContact: Codable, Hashable, CustomStringConvertible { - private enum CodingKeys: String, CodingKey { - case publicKey - case displayName - case profilePictureUrl - case profileKey - - case hasIsApproved - case isApproved - case hasIsBlocked - case isBlocked - case hasDidApproveMe - case didApproveMe - } - - public var publicKey: String? - public var displayName: String? - public var profilePictureUrl: String? - public var profileKey: Data? - - public var hasIsApproved: Bool - public var isApproved: Bool - public var hasIsBlocked: Bool - public var isBlocked: Bool - public var hasDidApproveMe: Bool - public var didApproveMe: Bool - - public var isValid: Bool { publicKey != nil && displayName != nil } - - public init( - publicKey: String?, - displayName: String?, - profilePictureUrl: String?, - profileKey: Data?, - hasIsApproved: Bool, - isApproved: Bool, - hasIsBlocked: Bool, - isBlocked: Bool, - hasDidApproveMe: Bool, - didApproveMe: Bool - ) { - self.publicKey = publicKey - self.displayName = displayName - self.profilePictureUrl = profilePictureUrl - self.profileKey = profileKey - self.hasIsApproved = hasIsApproved - self.isApproved = isApproved - self.hasIsBlocked = hasIsBlocked - self.isBlocked = isBlocked - self.hasDidApproveMe = hasDidApproveMe - self.didApproveMe = didApproveMe - } - - // MARK: - Codable - - public init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - publicKey = try? container.decode(String.self, forKey: .publicKey) - displayName = try? container.decode(String.self, forKey: .displayName) - profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl) - profileKey = try? container.decode(Data.self, forKey: .profileKey) - - hasIsApproved = try container.decode(Bool.self, forKey: .hasIsApproved) - isApproved = try container.decode(Bool.self, forKey: .isApproved) - hasIsBlocked = try container.decode(Bool.self, forKey: .hasIsBlocked) - isBlocked = try container.decode(Bool.self, forKey: .isBlocked) - hasDidApproveMe = try container.decode(Bool.self, forKey: .hasDidApproveMe) - didApproveMe = try container.decode(Bool.self, forKey: .didApproveMe) - } - - public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> CMContact? { - let result: CMContact = CMContact( - publicKey: proto.publicKey.toHexString(), - displayName: proto.name, - profilePictureUrl: proto.profilePicture, - profileKey: proto.profileKey, - hasIsApproved: proto.hasIsApproved, - isApproved: proto.isApproved, - hasIsBlocked: proto.hasIsBlocked, - isBlocked: proto.isBlocked, - hasDidApproveMe: proto.hasDidApproveMe, - didApproveMe: proto.didApproveMe - ) - - guard result.isValid else { return nil } - return result - } - - public func toProto() -> SNProtoConfigurationMessageContact? { - guard isValid else { return nil } - guard let publicKey = publicKey, let displayName = displayName else { return nil } - let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName) - if let profilePictureUrl = profilePictureUrl { result.setProfilePicture(profilePictureUrl) } - if let profileKey = profileKey { result.setProfileKey(profileKey) } - - if hasIsApproved { result.setIsApproved(isApproved) } - if hasIsBlocked { result.setIsBlocked(isBlocked) } - if hasDidApproveMe { result.setDidApproveMe(didApproveMe) } - - do { - return try result.build() - } catch { - SNLog("Couldn't construct contact proto from: \(self).") - return nil - } - } - - public var description: String { displayName ?? "" } - } -} 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+ConfigurationMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ConfigurationMessages.swift deleted file mode 100644 index 390bf8381..000000000 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ConfigurationMessages.swift +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import Sodium -import SessionUIKit -import SessionUtilitiesKit - -extension MessageReceiver { - internal static func handleLegacyConfigurationMessage( - _ db: Database, - message: ConfigurationMessage, - using dependencies: Dependencies - ) throws { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard !SessionUtil.userConfigsEnabled(db) else { - TopBannerController.show(warning: .outdatedUserConfig) - return - } - - let userPublicKey = getUserHexEncodedPublicKey(db) - - guard message.sender == userPublicKey else { return } - - SNLog("Configuration message received.") - - // Note: `message.sentTimestamp` is in ms (convert to TimeInterval before converting to - // seconds to maintain the accuracy) - let isInitialSync: Bool = (!UserDefaults.standard[.hasSyncedInitialConfiguration]) - let messageSentTimestamp: TimeInterval = TimeInterval((message.sentTimestamp ?? 0) / 1000) - let lastConfigTimestamp: TimeInterval = UserDefaults.standard[.lastConfigurationSync] - .defaulting(to: Date(timeIntervalSince1970: 0)) - .timeIntervalSince1970 - - // Handle user profile changes - try ProfileManager.updateProfileIfNeeded( - db, - publicKey: userPublicKey, - name: message.displayName, - avatarUpdate: { - guard - let profilePictureUrl: String = message.profilePictureUrl, - let profileKey: Data = message.profileKey - else { return .none } - - return .updateTo( - url: profilePictureUrl, - key: profileKey, - fileName: nil - ) - }(), - sentTimestamp: messageSentTimestamp, - calledFromConfigHandling: true, - using: dependencies - ) - - // 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) - - if !userContact.isTrusted || !userContact.isApproved || !userContact.didApproveMe { - try userContact.save(db) - try Contact - .filter(id: userPublicKey) - .updateAll( // Handling a config update so don't use `updateAllAndConfig` - db, - Contact.Columns.isTrusted.set(to: true), - Contact.Columns.isApproved.set(to: true), - Contact.Columns.didApproveMe.set(to: true) - ) - } - - if isInitialSync || messageSentTimestamp > lastConfigTimestamp { - if isInitialSync { - UserDefaults.standard[.hasSyncedInitialConfiguration] = true - NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil) - } - - UserDefaults.standard[.lastConfigurationSync] = Date(timeIntervalSince1970: messageSentTimestamp) - - // Contacts - try message.contacts.forEach { contactInfo in - guard let sessionId: String = contactInfo.publicKey else { return } - - // If the contact is a blinded contact then only add them if they haven't already been - // unblinded - if SessionId.Prefix(from: sessionId) == .blinded15 || SessionId.Prefix(from: sessionId) == .blinded25 { - let hasUnblindedContact: Bool = BlindedIdLookup - .filter(BlindedIdLookup.Columns.blindedId == sessionId) - .filter(BlindedIdLookup.Columns.sessionId != nil) - .isNotEmpty(db) - - if hasUnblindedContact { - return - } - } - - // Note: We only update the contact and profile records if the data has actually changed - // in order to avoid triggering UI updates for every thread on the home screen - let contact: Contact = Contact.fetchOrCreate(db, id: sessionId) - let profile: Profile = Profile.fetchOrCreate(db, id: sessionId) - - if - profile.name != contactInfo.displayName || - profile.profilePictureUrl != contactInfo.profilePictureUrl || - profile.profileEncryptionKey != contactInfo.profileKey - { - try profile.save(db) - try Profile - .filter(id: sessionId) - .updateAll( // Handling a config update so don't use `updateAllAndConfig` - db, - [ - Profile.Columns.name.set(to: contactInfo.displayName), - (contactInfo.profilePictureUrl == nil ? nil : - Profile.Columns.profilePictureUrl.set(to: contactInfo.profilePictureUrl) - ), - (contactInfo.profileKey == nil ? nil : - Profile.Columns.profileEncryptionKey.set(to: contactInfo.profileKey) - ) - ].compactMap { $0 } - ) - } - - /// We only update these values if the proto actually has values for them (this is to prevent an - /// edge case where an old client could override the values with default values since they aren't included) - /// - /// **Note:** Since message requests have no reverse, we should only handle setting `isApproved` - /// and `didApproveMe` to `true`. This may prevent some weird edge cases where a config message - /// swapping `isApproved` and `didApproveMe` to `false` - if - (contactInfo.hasIsApproved && (contact.isApproved != contactInfo.isApproved)) || - (contactInfo.hasIsBlocked && (contact.isBlocked != contactInfo.isBlocked)) || - (contactInfo.hasDidApproveMe && (contact.didApproveMe != contactInfo.didApproveMe)) - { - try contact.save(db) - try Contact - .filter(id: sessionId) - .updateAll( // Handling a config update so don't use `updateAllAndConfig` - db, - [ - (!contactInfo.hasIsApproved || !contactInfo.isApproved ? nil : - Contact.Columns.isApproved.set(to: true) - ), - (!contactInfo.hasIsBlocked ? nil : - Contact.Columns.isBlocked.set(to: contactInfo.isBlocked) - ), - (!contactInfo.hasDidApproveMe || !contactInfo.didApproveMe ? nil : - Contact.Columns.didApproveMe.set(to: contactInfo.didApproveMe) - ) - ].compactMap { $0 } - ) - } - - // If the contact is blocked - if contactInfo.hasIsBlocked && contactInfo.isBlocked { - // If this message changed them to the blocked state and there is an existing thread - // associated with them that is a message request thread then delete it (assume - // that the current user had deleted that message request) - if - contactInfo.isBlocked != contact.isBlocked, // 'contact.isBlocked' will be the old value - let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId), - thread.isMessageRequest(db) - { - _ = try thread.delete(db) - } - } - } - - // Closed groups - // - // Note: Only want to add these for initial sync to avoid re-adding closed groups the user - // intentionally left (any closed groups joined since the first processed sync message should - // get added via the 'handleNewClosedGroup' method anyway as they will have come through in the - // past two weeks) - if isInitialSync { - let existingClosedGroupsIds: [String] = (try? SessionThread - .filter(SessionThread.Columns.variant == SessionThread.Variant.legacyGroup) - .fetchAll(db)) - .defaulting(to: []) - .map { $0.id } - - try message.closedGroups.forEach { closedGroup in - guard !existingClosedGroupsIds.contains(closedGroup.publicKey) else { return } - - let keyPair: KeyPair = KeyPair( - publicKey: closedGroup.encryptionKeyPublicKey.bytes, - secretKey: closedGroup.encryptionKeySecretKey.bytes - ) - - try MessageReceiver.handleNewClosedGroup( - db, - groupPublicKey: closedGroup.publicKey, - name: closedGroup.name, - encryptionKeyPair: keyPair, - members: [String](closedGroup.members), - admins: [String](closedGroup.admins), - expirationTimer: closedGroup.expirationTimer, - formationTimestampMs: message.sentTimestamp!, - calledFromConfigHandling: false, // Legacy config isn't an issue - using: dependencies - ) - } - } - - // Open groups - for openGroupURL in message.openGroups { - if let (room, server, publicKey) = SessionUtil.parseCommunity(url: openGroupURL) { - let successfullyAddedGroup: Bool = OpenGroupManager.shared - .add( - db, - roomToken: room, - server: server, - publicKey: publicKey, - calledFromConfigHandling: true - ) - - if successfullyAddedGroup { - db.afterNextTransactionNested { _ in - OpenGroupManager.shared.performInitialRequestsAfterAdd( - successfullyAddedGroup: successfullyAddedGroup, - roomToken: room, - server: server, - publicKey: publicKey, - calledFromConfigHandling: false - ) - .subscribe(on: OpenGroupAPI.workQueue) - .sinkUntilComplete() - } - } - } - } - } - } -} 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 e1d1b8be8..e6a4d84a5 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import Sodium +import SessionUIKit import SessionUtilitiesKit import SessionSnodeKit @@ -64,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: @@ -242,13 +248,6 @@ public enum MessageReceiver { message: message ) - case let message as ConfigurationMessage: - try MessageReceiver.handleLegacyConfigurationMessage( - db, - message: message, - using: dependencies - ) - case let message as UnsendRequest: try MessageReceiver.handleUnsendRequest( db, @@ -282,6 +281,7 @@ public enum MessageReceiver { ) // SharedConfigMessages should be handled by the 'SharedUtil' instead of this + case is ConfigurationMessage: TopBannerController.show(warning: .outdatedUserConfig) case is SharedConfigMessage: throw MessageReceiverError.invalidSharedConfigMessageHandling default: fatalError() 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/Notification+MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift index d5c5b48fa..df890e31d 100644 --- a/SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift @@ -3,18 +3,9 @@ import Foundation public extension Notification.Name { - - // FIXME: Remove once `useSharedUtilForUserConfig` is permanent - static let initialConfigurationMessageReceived = Notification.Name("initialConfigurationMessageReceived") static let missedCall = Notification.Name("missedCall") } public extension Notification.Key { static let senderId = Notification.Key("senderId") } - -@objc public extension NSNotification { - - // FIXME: Remove once `useSharedUtilForUserConfig` is permanent - @objc static let initialConfigurationMessageReceived = Notification.Name.initialConfigurationMessageReceived.rawValue as NSString -} 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/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index 3e62ad28c..bf69dd878 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -14,12 +14,7 @@ public final class CurrentUserPoller: Poller { // MARK: - Settings - override var namespaces: [SnodeAPI.Namespace] { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled else { return [.default] } - - return CurrentUserPoller.namespaces - } + override var namespaces: [SnodeAPI.Namespace] { CurrentUserPoller.namespaces } /// After polling a given snode this many times we always switch to a new one. /// 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 909ea9ce7..4a9c8d286 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift @@ -50,9 +50,6 @@ internal extension SessionUtil { publicKey: String, change: (UnsafeMutablePointer?) throws -> () ) throws { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled(db, ignoreRequirementsForRunningMigrations: true) else { return } - // Since we are doing direct memory manipulation we are using an `Atomic` // type which has blocking access in it's `mutate` closure let needsPush: Bool @@ -213,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 } @@ -307,9 +344,6 @@ internal extension SessionUtil { targetConfig: ConfigDump.Variant, changeTimestampMs: Int64 ) -> Bool { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled(db) else { return true } - let targetPublicKey: String = { switch targetConfig { default: return getUserHexEncodedPublicKey(db) @@ -349,10 +383,7 @@ public extension SessionUtil { threadVariant: SessionThread.Variant, visibleOnly: Bool ) -> Bool { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled(db) else { return true } - - let userPublicKey: String = getUserHexEncodedPublicKey() + let userPublicKey: String = getUserHexEncodedPublicKey(db) let configVariant: ConfigDump.Variant = { switch threadVariant { case .contact: return (threadId == userPublicKey ? .userProfile : .contacts) 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/QueryInterfaceRequest+Utilities.swift b/SessionMessagingKit/SessionUtil/Database/QueryInterfaceRequest+Utilities.swift index a8be039fe..a7285604e 100644 --- a/SessionMessagingKit/SessionUtil/Database/QueryInterfaceRequest+Utilities.swift +++ b/SessionMessagingKit/SessionUtil/Database/QueryInterfaceRequest+Utilities.swift @@ -91,11 +91,7 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table let updatedData: [RowDecoder] = try self.updateAndFetchAll(db, assignments.map { $0.assignment }) // Then check if any of the changes could affect the config - guard - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - SessionUtil.userConfigsEnabled(db, ignoreRequirementsForRunningMigrations: true), - SessionUtil.assignmentsRequireConfigUpdate(assignments) - else { return updatedData } + guard SessionUtil.assignmentsRequireConfigUpdate(assignments) else { return updatedData } defer { // If we changed a column that requires a config update then we may as well automatically 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/SessionUtil/SessionUtil.swift b/SessionMessagingKit/SessionUtil/SessionUtil.swift index d933238a5..a353e8ed1 100644 --- a/SessionMessagingKit/SessionUtil/SessionUtil.swift +++ b/SessionMessagingKit/SessionUtil/SessionUtil.swift @@ -6,25 +6,6 @@ import SessionSnodeKit import SessionUtil import SessionUtilitiesKit -// MARK: - Features - -public extension Features { - static func useSharedUtilForUserConfig(_ db: Database? = nil) -> Bool { - guard Date().timeIntervalSince1970 < 1690761600 else { return true } - guard !SessionUtil.hasCheckedMigrationsCompleted.wrappedValue else { - return SessionUtil.userConfigsEnabledIgnoringFeatureFlag - } - - if let db: Database = db { - return SessionUtil.refreshingUserConfigsEnabled(db) - } - - return Storage.shared - .read { db in SessionUtil.refreshingUserConfigsEnabled(db) } - .defaulting(to: false) - } -} - // MARK: - SessionUtil public enum SessionUtil { @@ -70,10 +51,7 @@ public enum SessionUtil { /// Returns `true` if there is a config which needs to be pushed, but returns `false` if the configs are all up to date or haven't been /// loaded yet (eg. fresh install) public static var needsSync: Bool { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled else { return false } - - return configStore + configStore .wrappedValue .contains { _, atomicConf in guard atomicConf.wrappedValue != nil else { return false } @@ -84,56 +62,6 @@ public enum SessionUtil { public static var libSessionVersion: String { String(cString: LIBSESSION_UTIL_VERSION_STR) } - fileprivate static let hasCheckedMigrationsCompleted: Atomic = Atomic(false) - private static let requiredMigrationsCompleted: Atomic = Atomic(false) - private static let requiredMigrationIdentifiers: Set = [ - TargetMigrations.Identifier.messagingKit.key(with: _013_SessionUtilChanges.self), - TargetMigrations.Identifier.messagingKit.key(with: _014_GenerateInitialUserConfigDumps.self) - ] - - public static var userConfigsEnabled: Bool { - return userConfigsEnabled(nil) - } - - public static func userConfigsEnabled(_ db: Database?) -> Bool { - Features.useSharedUtilForUserConfig(db) && - SessionUtil.userConfigsEnabledIgnoringFeatureFlag - } - - public static var userConfigsEnabledIgnoringFeatureFlag: Bool { - SessionUtil.requiredMigrationsCompleted.wrappedValue - } - - internal static func userConfigsEnabled( - _ db: Database, - ignoreRequirementsForRunningMigrations: Bool - ) -> Bool { - // First check if we are enabled regardless of what we want to ignore - guard - Features.useSharedUtilForUserConfig(db), - !SessionUtil.requiredMigrationsCompleted.wrappedValue, - !SessionUtil.refreshingUserConfigsEnabled(db), - ignoreRequirementsForRunningMigrations, - let currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type) = Storage.shared.currentlyRunningMigration - else { return true } - - let nonIgnoredMigrationIdentifiers: Set = SessionUtil.requiredMigrationIdentifiers - .removing(currentlyRunningMigration.identifier.key(with: currentlyRunningMigration.migration)) - - return Storage.appliedMigrationIdentifiers(db) - .isSuperset(of: nonIgnoredMigrationIdentifiers) - } - - @discardableResult public static func refreshingUserConfigsEnabled(_ db: Database) -> Bool { - let result: Bool = Storage.appliedMigrationIdentifiers(db) - .isSuperset(of: SessionUtil.requiredMigrationIdentifiers) - - requiredMigrationsCompleted.mutate { $0 = result } - hasCheckedMigrationsCompleted.mutate { $0 = true } - - return result - } - internal static func lastError(_ conf: UnsafeMutablePointer?) -> String { return (conf?.pointee.last_error.map { String(cString: $0) } ?? "Unknown") } @@ -141,9 +69,6 @@ public enum SessionUtil { // MARK: - Loading public static func clearMemoryState() { - // Ensure we have a loaded state before we continue - guard !SessionUtil.configStore.wrappedValue.isEmpty else { return } - SessionUtil.configStore.mutate { confStore in confStore.removeAll() } @@ -169,9 +94,6 @@ public enum SessionUtil { return } - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled(db, ignoreRequirementsForRunningMigrations: true) else { return } - // Retrieve the existing dumps from the database let existingDumps: Set = ((try? ConfigDump.fetchSet(db)) ?? []) let existingDumpVariants: Set = existingDumps @@ -395,9 +317,6 @@ public enum SessionUtil { } public static func configHashes(for publicKey: String) -> [String] { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled else { return [] } - return Storage.shared .read { db -> Set in guard Identity.userExists(db) else { return [] } @@ -437,8 +356,6 @@ public enum SessionUtil { messages: [SharedConfigMessage], publicKey: String ) throws { - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - guard SessionUtil.userConfigsEnabled(db) else { return } guard !messages.isEmpty else { return } guard !publicKey.isEmpty else { throw MessageReceiverError.noThread } diff --git a/SessionMessagingKit/Shared Models/MentionInfo.swift b/SessionMessagingKit/Shared Models/MentionInfo.swift index 984ddf63d..f4bb049cb 100644 --- a/SessionMessagingKit/Shared Models/MentionInfo.swift +++ b/SessionMessagingKit/Shared Models/MentionInfo.swift @@ -3,12 +3,14 @@ import GRDB import SessionUtilitiesKit -public struct MentionInfo: FetchableRecord, Decodable { - fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) - fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) - - fileprivate static let profileString: String = CodingKeys.profile.stringValue +public struct MentionInfo: FetchableRecord, Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case profile + case threadVariant + case openGroupServer + case openGroupRoomToken + } public let profile: Profile public let threadVariant: SessionThread.Variant @@ -79,7 +81,7 @@ public extension MentionInfo { return SQLRequest(""" SELECT \(Profile.self).*, - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) \(targetJoin) \(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)")) @@ -89,7 +91,7 @@ public extension MentionInfo { return SQLRequest(""" SELECT \(Profile.self).*, - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) \(targetJoin) JOIN \(GroupMember.self) ON ( @@ -107,9 +109,9 @@ public extension MentionInfo { SELECT \(Profile.self).*, MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), - \(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), - \(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")), + \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken) \(targetJoin) JOIN \(Interaction.self) ON ( @@ -130,8 +132,8 @@ public extension MentionInfo { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - MentionInfo.profileString: adapters[0] + return ScopeAdapter.with(MentionInfo.self, [ + .profile: adapters[0] ]) } } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 2a796cbd1..3eaada1b1 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -11,42 +11,66 @@ fileprivate typealias AttachmentInteractionInfo = MessageViewModel.AttachmentInt fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo -public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) - public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - public static let threadIsTrustedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsTrusted.stringValue) - public static let threadHasDisappearingMessagesEnabledKey: SQL = SQL(stringLiteral: CodingKeys.threadHasDisappearingMessagesEnabled.stringValue) - public static let threadOpenGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupServer.stringValue) - public static let threadOpenGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupPublicKey.stringValue) - public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) - public static let stateKey: SQL = SQL(stringLiteral: CodingKeys.state.stringValue) - public static let hasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.hasAtLeastOneReadReceipt.stringValue) - public static let mostRecentFailureTextKey: SQL = SQL(stringLiteral: CodingKeys.mostRecentFailureText.stringValue) - public static let isTypingIndicatorKey: SQL = SQL(stringLiteral: CodingKeys.isTypingIndicator.stringValue) - public static let isSenderOpenGroupModeratorKey: SQL = SQL(stringLiteral: CodingKeys.isSenderOpenGroupModerator.stringValue) - public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) - public static let quoteKey: SQL = SQL(stringLiteral: CodingKeys.quote.stringValue) - public static let quoteAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.quoteAttachment.stringValue) - public static let linkPreviewKey: SQL = SQL(stringLiteral: CodingKeys.linkPreview.stringValue) - public static let linkPreviewAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.linkPreviewAttachment.stringValue) - public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) - public static let cellTypeKey: SQL = SQL(stringLiteral: CodingKeys.cellType.stringValue) - public static let authorNameKey: SQL = SQL(stringLiteral: CodingKeys.authorName.stringValue) - public static let canHaveProfileKey: SQL = SQL(stringLiteral: CodingKeys.canHaveProfile.stringValue) - public static let shouldShowProfileKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowProfile.stringValue) - public static let shouldShowDateHeaderKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowDateHeader.stringValue) - public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) - public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) - public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.stringValue) - public static let isLastOutgoingKey: SQL = SQL(stringLiteral: CodingKeys.isLastOutgoing.stringValue) - - public static let profileString: String = CodingKeys.profile.stringValue - public static let quoteString: String = CodingKeys.quote.stringValue - public static let quoteAttachmentString: String = CodingKeys.quoteAttachment.stringValue - public static let linkPreviewString: String = CodingKeys.linkPreview.stringValue - public static let linkPreviewAttachmentString: String = CodingKeys.linkPreviewAttachment.stringValue +public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case threadId + case threadVariant + case threadIsTrusted + case threadHasDisappearingMessagesEnabled + case threadOpenGroupServer + case threadOpenGroupPublicKey + case threadContactNameInternal + + // Interaction Info + + case rowId + case id + case openGroupServerMessageId + case variant + case timestampMs + case receivedAtTimestampMs + case authorId + case authorNameInternal + case body + case rawBody + case expiresStartedAtMs + case expiresInSeconds + + case state + case hasAtLeastOneReadReceipt + case mostRecentFailureText + case isSenderOpenGroupModerator + case isTypingIndicator + case profile + case quote + case quoteAttachment + case linkPreview + case linkPreviewAttachment + + case currentUserPublicKey + + // Post-Query Processing Data + + case attachments + case reactionInfo + case cellType + case authorName + case senderName + case canHaveProfile + case shouldShowProfile + case shouldShowDateHeader + case containsOnlyEmoji + case glyphCount + case previousVariant + case positionInCluster + case isOnlyMessageInCluster + case isLast + case isLastOutgoing + case currentUserBlinded15PublicKey + case currentUserBlinded25PublicKey + case optimisticMessageId + } public enum CellType: Int, Decodable, Equatable, Hashable, DatabaseValueConvertible { case textOnlyMessage @@ -462,13 +486,13 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, // MARK: - AttachmentInteractionInfo public extension MessageViewModel { - struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) - public static let interactionAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachment.stringValue) - - public static let attachmentString: String = CodingKeys.attachment.stringValue - public static let interactionAttachmentString: String = CodingKeys.interactionAttachment.stringValue + struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case attachment + case interactionAttachment + } public let rowId: Int64 public let attachment: Attachment @@ -491,13 +515,13 @@ public extension MessageViewModel { // MARK: - ReactionInfo public extension MessageViewModel { - struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let reactionKey: SQL = SQL(stringLiteral: CodingKeys.reaction.stringValue) - public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) - - public static let reactionString: String = CodingKeys.reaction.stringValue - public static let profileString: String = CodingKeys.profile.stringValue + struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case reaction + case profile + } public let rowId: Int64 public let reaction: Reaction @@ -522,9 +546,12 @@ public extension MessageViewModel { // MARK: - TypingIndicatorInfo public extension MessageViewModel { - struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) + struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case threadId + } public let rowId: Int64 public let threadId: String @@ -776,59 +803,48 @@ public extension MessageViewModel { let contact: TypedTableAlias = TypedTableAlias() let disappearingMessagesConfig: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let threadProfile: TypedTableAlias = TypedTableAlias(name: "threadProfile") let quote: TypedTableAlias = TypedTableAlias() + let quoteInteraction: TypedTableAlias = TypedTableAlias(name: "quoteInteraction") + let quoteInteractionAttachment: TypedTableAlias = TypedTableAlias( + name: "quoteInteractionAttachment" + ) + let quoteLinkPreview: TypedTableAlias = TypedTableAlias(name: "quoteLinkPreview") + let quoteAttachment: TypedTableAlias = TypedTableAlias(name: ViewModel.CodingKeys.quoteAttachment.stringValue) let linkPreview: TypedTableAlias = TypedTableAlias() - - let threadProfile: SQL = SQL(stringLiteral: "threadProfile") - let quoteInteraction: SQL = SQL(stringLiteral: "quoteInteraction") - let quoteInteractionAttachment: SQL = SQL(stringLiteral: "quoteInteractionAttachment") - let readReceipt: SQL = SQL(stringLiteral: "readReceipt") - let idColumn: SQL = SQL(stringLiteral: Interaction.Columns.id.name) - let interactionBodyColumn: SQL = SQL(stringLiteral: Interaction.Columns.body.name) - let profileIdColumn: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let nicknameColumn: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let nameColumn: SQL = SQL(stringLiteral: Profile.Columns.name.name) - let quoteBodyColumn: SQL = SQL(stringLiteral: Quote.Columns.body.name) - let quoteAttachmentIdColumn: SQL = SQL(stringLiteral: Quote.Columns.attachmentId.name) - let readReceiptInteractionIdColumn: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) - let readTimestampMsColumn: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) - let timestampMsColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) - let authorIdColumn: SQL = SQL(stringLiteral: Interaction.Columns.authorId.name) - let attachmentIdColumn: SQL = SQL(stringLiteral: Attachment.Columns.id.name) - let interactionAttachmentInteractionIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) - let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) - let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) + let linkPreviewAttachment: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .linkPreviewAttachment) + let readReceipt: TypedTableAlias = TypedTableAlias(name: "readReceipt") let numColumnsBeforeLinkedRecords: Int = 22 let finalGroupSQL: SQL = (groupSQL ?? "") let request: SQLRequest = """ SELECT - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), -- Default to 'true' for non-contact threads - IFNULL(\(contact[.isTrusted]), true) AS \(ViewModel.threadIsTrustedKey), + IFNULL(\(contact[.isTrusted]), true) AS \(ViewModel.Columns.threadIsTrusted), -- Default to 'false' when no contact exists - IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey), - \(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey), - \(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey), - IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey), + IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.Columns.threadHasDisappearingMessagesEnabled), + \(openGroup[.server]) AS \(ViewModel.Columns.threadOpenGroupServer), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.threadOpenGroupPublicKey), + IFNULL(\(threadProfile[.nickname]), \(threadProfile[.name])) AS \(ViewModel.Columns.threadContactNameInternal), - \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), + \(interaction[.rowId]) AS \(ViewModel.Columns.rowId), \(interaction[.id]), \(interaction[.openGroupServerMessageId]), \(interaction[.variant]), \(interaction[.timestampMs]), \(interaction[.receivedAtTimestampMs]), \(interaction[.authorId]), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), \(interaction[.body]), \(interaction[.expiresStartedAtMs]), \(interaction[.expiresInSeconds]), -- Default to 'sending' assuming non-processed interaction when null - IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey), - (\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), - \(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey), + IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.state), + (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.hasAtLeastOneReadReceipt), + \(recipientState[.mostRecentFailureText]) AS \(ViewModel.Columns.mostRecentFailureText), EXISTS ( SELECT 1 @@ -839,46 +855,46 @@ public extension MessageViewModel { \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND \(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])")) ) - ) AS \(ViewModel.isSenderOpenGroupModeratorKey), + ) AS \(ViewModel.Columns.isSenderOpenGroupModerator), - \(ViewModel.profileKey).*, + \(profile.allColumns), \(quote[.interactionId]), \(quote[.authorId]), \(quote[.timestampMs]), - \(quoteInteraction).\(interactionBodyColumn) AS \(quoteBodyColumn), - \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn), - \(ViewModel.quoteAttachmentKey).*, - \(ViewModel.linkPreviewKey).*, - \(ViewModel.linkPreviewAttachmentKey).*, + \(quoteInteraction[.body]), + \(quoteInteractionAttachment[.attachmentId]), + \(quoteAttachment.allColumns), + \(linkPreview.allColumns), + \(linkPreviewAttachment.allColumns), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey), -- All of the below properties are set in post-query processing but to prevent the -- query from crashing when decoding we need to provide default values - \(CellType.textOnlyMessage) AS \(ViewModel.cellTypeKey), - '' AS \(ViewModel.authorNameKey), - false AS \(ViewModel.canHaveProfileKey), - false AS \(ViewModel.shouldShowProfileKey), - false AS \(ViewModel.shouldShowDateHeaderKey), - \(Position.middle) AS \(ViewModel.positionInClusterKey), - false AS \(ViewModel.isOnlyMessageInClusterKey), - false AS \(ViewModel.isLastKey), - false AS \(ViewModel.isLastOutgoingKey) + \(CellType.textOnlyMessage) AS \(ViewModel.Columns.cellType), + '' AS \(ViewModel.Columns.authorName), + false AS \(ViewModel.Columns.canHaveProfile), + false AS \(ViewModel.Columns.shouldShowProfile), + false AS \(ViewModel.Columns.shouldShowDateHeader), + \(Position.middle) AS \(ViewModel.Columns.positionInCluster), + false AS \(ViewModel.Columns.isOnlyMessageInCluster), + false AS \(ViewModel.Columns.isLast), + false AS \(ViewModel.Columns.isLastOutgoing) FROM \(Interaction.self) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId]) - LEFT JOIN \(Profile.self) AS \(threadProfile) ON \(threadProfile).\(profileIdColumn) = \(interaction[.threadId]) + LEFT JOIN \(threadProfile) ON \(threadProfile[.id]) = \(interaction[.threadId]) LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id]) - LEFT JOIN \(Interaction.self) AS \(quoteInteraction) ON ( - \(quoteInteraction).\(timestampMsColumn) = \(quote[.timestampMs]) AND ( - \(quoteInteraction).\(authorIdColumn) = \(quote[.authorId]) OR ( + LEFT JOIN \(quoteInteraction) ON ( + \(quoteInteraction[.timestampMs]) = \(quote[.timestampMs]) AND ( + \(quoteInteraction[.authorId]) = \(quote[.authorId]) OR ( -- A users outgoing message is stored in some cases using their standard id -- but the quote will use their blinded id so handle that case - \(quoteInteraction).\(authorIdColumn) = \(userPublicKey) AND + \(quoteInteraction[.authorId]) = \(userPublicKey) AND ( \(quote[.authorId]) = \(blinded15PublicKey ?? "''") OR \(quote[.authorId]) = \(blinded25PublicKey ?? "''") @@ -886,27 +902,38 @@ public extension MessageViewModel { ) ) ) - LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON ( - \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) AND - \(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0 + LEFT JOIN \(quoteInteractionAttachment) ON ( + \(quoteInteractionAttachment[.interactionId]) = \(quoteInteraction[.id]) AND + \(quoteInteractionAttachment[.albumIndex]) = 0 + ) + LEFT JOIN \(quoteLinkPreview) ON ( + \(quoteLinkPreview[.url]) = \(quoteInteraction[.linkPreviewUrl]) AND + \(Interaction.linkPreviewFilterLiteral( + interaction: quoteInteraction, + linkPreview: quoteLinkPreview + )) + ) + LEFT JOIN \(quoteAttachment) ON ( + \(quoteAttachment[.id]) = \(quoteInteractionAttachment[.attachmentId]) OR + \(quoteAttachment[.id]) = \(quoteLinkPreview[.attachmentId]) OR + \(quoteAttachment[.id]) = \(quote[.attachmentId]) ) - LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) - LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumn) = \(linkPreview[.attachmentId]) + LEFT JOIN \(linkPreviewAttachment) ON \(linkPreviewAttachment[.id]) = \(linkPreview[.attachmentId]) LEFT JOIN \(RecipientState.self) ON ( -- Ignore 'skipped' states \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND \(recipientState[.interactionId]) = \(interaction[.id]) ) - LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON ( - \(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND - \(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id]) + LEFT JOIN \(readReceipt) ON ( + \(readReceipt[.readTimestampMs]) IS NOT NULL AND + \(readReceipt[.interactionId]) = \(interaction[.id]) ) - WHERE \(interaction.alias[Column.rowID]) IN \(rowIds) + WHERE \(interaction[.rowId]) IN \(rowIds) \(finalGroupSQL) ORDER BY \(orderSQL) """ @@ -921,12 +948,12 @@ public extension MessageViewModel { Attachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.profileString: adapters[1], - ViewModel.quoteString: adapters[2], - ViewModel.quoteAttachmentString: adapters[3], - ViewModel.linkPreviewString: adapters[4], - ViewModel.linkPreviewAttachmentString: adapters[5] + return ScopeAdapter.with(ViewModel.self, [ + .profile: adapters[1], + .quote: adapters[2], + .quoteAttachment: adapters[3], + .linkPreview: adapters[4], + .linkPreviewAttachment: adapters[5] ]) } } @@ -953,9 +980,9 @@ public extension MessageViewModel.AttachmentInteractionInfo { let numColumnsBeforeLinkedRecords: Int = 1 let request: SQLRequest = """ SELECT - \(attachment.alias[Column.rowID]) AS \(AttachmentInteractionInfo.rowIdKey), - \(AttachmentInteractionInfo.attachmentKey).*, - \(AttachmentInteractionInfo.interactionAttachmentKey).* + \(attachment[.rowId]) AS \(AttachmentInteractionInfo.Columns.rowId), + \(attachment.allColumns), + \(interactionAttachment.allColumns) FROM \(Attachment.self) JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id]) \(finalFilterSQL) @@ -968,9 +995,9 @@ public extension MessageViewModel.AttachmentInteractionInfo { InteractionAttachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - AttachmentInteractionInfo.attachmentString: adapters[1], - AttachmentInteractionInfo.interactionAttachmentString: adapters[2] + return ScopeAdapter.with(AttachmentInteractionInfo.self, [ + .attachment: adapters[1], + .interactionAttachment: adapters[2] ]) } } @@ -1034,9 +1061,9 @@ public extension MessageViewModel.ReactionInfo { let numColumnsBeforeLinkedRecords: Int = 1 let request: SQLRequest = """ SELECT - \(reaction.alias[Column.rowID]) AS \(ReactionInfo.rowIdKey), - \(ReactionInfo.reactionKey).*, - \(ReactionInfo.profileKey).* + \(reaction[.rowId]) AS \(ReactionInfo.Columns.rowId), + \(reaction.allColumns), + \(profile.allColumns) FROM \(Reaction.self) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId]) \(finalFilterSQL) @@ -1049,9 +1076,9 @@ public extension MessageViewModel.ReactionInfo { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ReactionInfo.reactionString: adapters[1], - ReactionInfo.profileString: adapters[2] + return ScopeAdapter.with(ReactionInfo.self, [ + .reaction: adapters[1], + .profile: adapters[2] ]) } } @@ -1117,8 +1144,8 @@ public extension MessageViewModel.TypingIndicatorInfo { }() let request: SQLRequest = """ SELECT - \(threadTypingIndicator.alias[Column.rowID]) AS \(MessageViewModel.TypingIndicatorInfo.rowIdKey), - \(threadTypingIndicator[.threadId]) AS \(MessageViewModel.TypingIndicatorInfo.threadIdKey) + \(threadTypingIndicator[.rowId]), + \(threadTypingIndicator[.threadId]) FROM \(ThreadTypingIndicator.self) \(finalFilterSQL) """ diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 0ee0f5f5a..841f844e9 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -14,65 +14,70 @@ fileprivate typealias ViewModel = SessionThreadViewModel /// /// **Note:** When updating the UI make sure to check the actual queries being run as some fields will have incorrect default values /// in order to optimise their queries to only include the required data -public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) - public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - public static let threadCreationDateTimestampKey: SQL = SQL(stringLiteral: CodingKeys.threadCreationDateTimestamp.stringValue) - public static let threadMemberNamesKey: SQL = SQL(stringLiteral: CodingKeys.threadMemberNames.stringValue) - public static let threadIsNoteToSelfKey: SQL = SQL(stringLiteral: CodingKeys.threadIsNoteToSelf.stringValue) - public static let threadIsMessageRequestKey: SQL = SQL(stringLiteral: CodingKeys.threadIsMessageRequest.stringValue) - public static let threadRequiresApprovalKey: SQL = SQL(stringLiteral: CodingKeys.threadRequiresApproval.stringValue) - public static let threadShouldBeVisibleKey: SQL = SQL(stringLiteral: CodingKeys.threadShouldBeVisible.stringValue) - public static let threadPinnedPriorityKey: SQL = SQL(stringLiteral: CodingKeys.threadPinnedPriority.stringValue) - public static let threadIsBlockedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsBlocked.stringValue) - public static let threadMutedUntilTimestampKey: SQL = SQL(stringLiteral: CodingKeys.threadMutedUntilTimestamp.stringValue) - public static let threadOnlyNotifyForMentionsKey: SQL = SQL(stringLiteral: CodingKeys.threadOnlyNotifyForMentions.stringValue) - public static let threadMessageDraftKey: SQL = SQL(stringLiteral: CodingKeys.threadMessageDraft.stringValue) - public static let threadContactIsTypingKey: SQL = SQL(stringLiteral: CodingKeys.threadContactIsTyping.stringValue) - public static let threadWasMarkedUnreadKey: SQL = SQL(stringLiteral: CodingKeys.threadWasMarkedUnread.stringValue) - public static let threadUnreadCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadCount.stringValue) - public static let threadUnreadMentionCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadMentionCount.stringValue) - public static let disappearingMessagesConfigurationKey: SQL = SQL(stringLiteral: CodingKeys.disappearingMessagesConfiguration.stringValue) - public static let contactProfileKey: SQL = SQL(stringLiteral: CodingKeys.contactProfile.stringValue) - public static let closedGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupName.stringValue) - public static let closedGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupUserCount.stringValue) - public static let currentUserIsClosedGroupMemberKey: SQL = SQL(stringLiteral: CodingKeys.currentUserIsClosedGroupMember.stringValue) - public static let currentUserIsClosedGroupAdminKey: SQL = SQL(stringLiteral: CodingKeys.currentUserIsClosedGroupAdmin.stringValue) - public static let closedGroupProfileFrontKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileFront.stringValue) - public static let closedGroupProfileBackKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileBack.stringValue) - public static let closedGroupProfileBackFallbackKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileBackFallback.stringValue) - public static let openGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.openGroupName.stringValue) - public static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) - public static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) - public static let openGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPublicKey.stringValue) - public static let openGroupProfilePictureDataKey: SQL = SQL(stringLiteral: CodingKeys.openGroupProfilePictureData.stringValue) - public static let openGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.openGroupUserCount.stringValue) - public static let openGroupPermissionsKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPermissions.stringValue) - public static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) - public static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) - public static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) - public static let interactionBodyKey: SQL = SQL(stringLiteral: CodingKeys.interactionBody.stringValue) - public static let interactionStateKey: SQL = SQL(stringLiteral: CodingKeys.interactionState.stringValue) - public static let interactionHasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.interactionHasAtLeastOneReadReceipt.stringValue) - public static let interactionIsOpenGroupInvitationKey: SQL = SQL(stringLiteral: CodingKeys.interactionIsOpenGroupInvitation.stringValue) - public static let interactionAttachmentDescriptionInfoKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentDescriptionInfo.stringValue) - public static let interactionAttachmentCountKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentCount.stringValue) - public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) - public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) - public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) - - public static let threadWasMarkedUnreadString: String = CodingKeys.threadWasMarkedUnread.stringValue - public static let threadUnreadCountString: String = CodingKeys.threadUnreadCount.stringValue - public static let threadUnreadMentionCountString: String = CodingKeys.threadUnreadMentionCount.stringValue - public static let closedGroupUserCountString: String = CodingKeys.closedGroupUserCount.stringValue - public static let openGroupUserCountString: String = CodingKeys.openGroupUserCount.stringValue - public static let disappearingMessagesConfigurationString: String = CodingKeys.disappearingMessagesConfiguration.stringValue - public static let contactProfileString: String = CodingKeys.contactProfile.stringValue - public static let closedGroupProfileFrontString: String = CodingKeys.closedGroupProfileFront.stringValue - public static let closedGroupProfileBackString: String = CodingKeys.closedGroupProfileBack.stringValue - public static let closedGroupProfileBackFallbackString: String = CodingKeys.closedGroupProfileBackFallback.stringValue - public static let interactionAttachmentDescriptionInfoString: String = CodingKeys.interactionAttachmentDescriptionInfo.stringValue +public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case threadId + case threadVariant + case threadCreationDateTimestamp + case threadMemberNames + + case threadIsNoteToSelf + case threadIsMessageRequest + case threadRequiresApproval + case threadShouldBeVisible + case threadPinnedPriority + case threadIsBlocked + case threadMutedUntilTimestamp + case threadOnlyNotifyForMentions + case threadMessageDraft + + case threadContactIsTyping + case threadWasMarkedUnread + case threadUnreadCount + case threadUnreadMentionCount + + // Thread display info + + case disappearingMessagesConfiguration + + case contactProfile + case closedGroupProfileFront + case closedGroupProfileBack + case closedGroupProfileBackFallback + case closedGroupName + case closedGroupUserCount + case currentUserIsClosedGroupMember + case currentUserIsClosedGroupAdmin + case openGroupName + case openGroupServer + case openGroupRoomToken + case openGroupPublicKey + case openGroupProfilePictureData + case openGroupUserCount + case openGroupPermissions + + // Interaction display info + + case interactionId + case interactionVariant + case interactionTimestampMs + case interactionBody + case interactionState + case interactionHasAtLeastOneReadReceipt + case interactionIsOpenGroupInvitation + case interactionAttachmentDescriptionInfo + case interactionAttachmentCount + + case authorId + case threadContactNameInternal + case authorNameInternal + case currentUserPublicKey + case currentUserBlinded15PublicKey + case currentUserBlinded25PublicKey + case recentReactionEmoji + } public var differenceIdentifier: String { threadId } public var id: String { threadId } @@ -104,7 +109,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 && @@ -550,6 +559,51 @@ public extension SessionThreadViewModel { } } +// MARK: - AggregateInteraction + +private struct AggregateInteraction: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case interactionId + case threadId + case interactionTimestampMs + case threadUnreadCount + case threadUnreadMentionCount + } + + let interactionId: Int64 + let threadId: String + let interactionTimestampMs: Int64 + let threadUnreadCount: UInt? + let threadUnreadMentionCount: UInt? +} + +// MARK: - ClosedGroupUserCount + +private struct ClosedGroupUserCount: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case groupId + case closedGroupUserCount + } + + let groupId: String + let closedGroupUserCount: Int +} + +// MARK: - GroupMemberInfo + +private struct GroupMemberInfo: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case groupId + case threadMemberNames + } + + let groupId: String + let threadMemberNames: String +} + // MARK: - HomeVC & MessageRequestsViewController // MARK: --SessionThreadViewModel @@ -566,65 +620,57 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() let typingIndicator: TypedTableAlias = TypedTableAlias() - let closedGroup: TypedTableAlias = TypedTableAlias() - let groupMember: TypedTableAlias = TypedTableAlias() - let openGroup: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() let recipientState: TypedTableAlias = TypedTableAlias() + let readReceipt: TypedTableAlias = TypedTableAlias(name: "readReceipt") let linkPreview: TypedTableAlias = TypedTableAlias() + let firstInteractionAttachment: TypedTableAlias = TypedTableAlias(name: "firstInteractionAttachment") let attachment: TypedTableAlias = TypedTableAlias() let interactionAttachment: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() - - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) - let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) - let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt") - let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) - let firstInteractionAttachmentLiteral: SQL = SQL(stringLiteral: "firstInteractionAttachment") - let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) - let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) - let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) - + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) + let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) + let groupMember: TypedTableAlias = TypedTableAlias() + let openGroup: TypedTableAlias = TypedTableAlias() /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 14 let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND \(SQL("\(thread[.id]) != \(userPublicKey)")) AND IFNULL(\(contact[.isApproved]), false) = false - ) AS \(ViewModel.threadIsMessageRequestKey), + ) AS \(ViewModel.Columns.threadIsMessageRequest), - (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey), - \(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadMentionCountKey), + (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.Columns.threadContactIsTyping), + \(thread[.markedAsUnread]) AS \(ViewModel.Columns.threadWasMarkedUnread), + \(aggregateInteraction[.threadUnreadCount]), + \(aggregateInteraction[.threadUnreadMentionCount]), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), EXISTS ( SELECT 1 @@ -634,7 +680,7 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), EXISTS ( SELECT 1 @@ -644,15 +690,15 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupAdmin), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey), - \(interaction[.body]) AS \(ViewModel.interactionBodyKey), + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), + \(interaction[.timestampMs]) AS \(ViewModel.Columns.interactionTimestampMs), + \(interaction[.body]) AS \(ViewModel.Columns.interactionBody), -- Default to 'sending' assuming non-processed interaction when null IFNULL(( @@ -664,22 +710,22 @@ public extension SessionThreadViewModel { \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) ) LIMIT 1 - ), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.interactionStateKey), + ), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.interactionState), - (\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.interactionHasAtLeastOneReadReceiptKey), - (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey), + (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.interactionHasAtLeastOneReadReceipt), + (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.Columns.interactionIsOpenGroupInvitation), -- These 4 properties will be combined into 'Attachment.DescriptionInfo' \(attachment[.id]), \(attachment[.variant]), \(attachment[.contentType]), \(attachment[.sourceFilename]), - COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey), + COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.Columns.interactionAttachmentCount), \(interaction[.authorId]), - IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + IFNULL(\(contactProfile[.nickname]), \(contactProfile[.name])) AS \(ViewModel.Columns.threadContactNameInternal), + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) @@ -687,46 +733,46 @@ public extension SessionThreadViewModel { LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral), - SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey), - SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + SUM(\(interaction[.wasRead]) = false) AS \(AggregateInteraction.Columns.threadUnreadCount), + SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) LEFT JOIN \(Interaction.self) ON ( \(interaction[.threadId]) = \(thread[.id]) AND - \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey) + \(interaction[.id]) = \(aggregateInteraction[.interactionId]) ) - LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON ( - \(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) AND - \(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL + LEFT JOIN \(readReceipt) ON ( + \(interaction[.id]) = \(readReceipt[.interactionId]) AND + \(readReceipt[.readTimestampMs]) IS NOT NULL ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND - \(Interaction.linkPreviewFilterLiteral) AND + \(Interaction.linkPreviewFilterLiteral()) AND \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) ) - LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON ( - \(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) AND - \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 + LEFT JOIN \(firstInteractionAttachment) ON ( + \(firstInteractionAttachment[.interactionId]) = \(interaction[.id]) AND + \(firstInteractionAttachment[.albumIndex]) = 0 ) - LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) + LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachment[.attachmentId]) LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) -- Thread naming & avatar content - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -737,9 +783,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -750,13 +796,13 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) - WHERE \(thread.alias[Column.rowID]) IN \(rowIds) + WHERE \(thread[.rowId]) IN \(rowIds) \(groupSQL) ORDER BY \(orderSQL) """ @@ -772,12 +818,12 @@ public extension SessionThreadViewModel { Attachment.DescriptionInfo.numberOfSelectedColumns() ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4], - ViewModel.interactionAttachmentDescriptionInfoString: adapters[6] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4], + .interactionAttachmentDescriptionInfo: adapters[6] ]) } } @@ -864,55 +910,52 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let disappearingMessagesConfiguration: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() - - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table") - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) + let closedGroupUserCount: TypedTableAlias = TypedTableAlias(name: "closedGroupUserCount") /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `disappearingMessageSConfiguration` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 15 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND \(SQL("\(thread[.id]) != \(userPublicKey)")) AND IFNULL(\(contact[.isApproved]), false) = false - ) AS \(ViewModel.threadIsMessageRequestKey), + ) AS \(ViewModel.Columns.threadIsMessageRequest), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND IFNULL(\(contact[.didApproveMe]), false) = false - ) AS \(ViewModel.threadRequiresApprovalKey), - \(thread[.shouldBeVisible]) AS \(ViewModel.threadShouldBeVisibleKey), + ) AS \(ViewModel.Columns.threadRequiresApproval), + \(thread[.shouldBeVisible]) AS \(ViewModel.Columns.threadShouldBeVisible), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), - \(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), + \(thread[.messageDraft]) AS \(ViewModel.Columns.threadMessageDraft), - \(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), + \(thread[.markedAsUnread]) AS \(ViewModel.Columns.threadWasMarkedUnread), + \(aggregateInteraction[.threadUnreadCount]), - \(ViewModel.disappearingMessagesConfigurationKey).*, + \(disappearingMessagesConfiguration.allColumns), - \(ViewModel.contactProfileKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey), + \(contactProfile.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(closedGroupUserCount[.closedGroupUserCount]), EXISTS ( SELECT 1 @@ -922,49 +965,50 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), - \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), - \(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey), - \(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey), - \(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.server]) AS \(ViewModel.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(ViewModel.Columns.openGroupRoomToken), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.openGroupPublicKey), + \(openGroup[.userCount]) AS \(ViewModel.Columns.openGroupUserCount), + \(openGroup[.permissions]) AS \(ViewModel.Columns.openGroupPermissions), - \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey), - \(aggregateInteractionLiteral).\(ViewModel.interactionTimestampMsKey), + \(aggregateInteraction[.interactionId]), + \(aggregateInteraction[.interactionTimestampMs]), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfiguration[.threadId]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey), - SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + SUM(\(interaction[.wasRead]) = false) AS \(AggregateInteraction.Columns.threadUnreadCount), + 0 AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE ( \(SQL("\(interaction[.threadId]) = \(threadId)")) AND \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) ) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN ( SELECT \(groupMember[.groupId]), - COUNT(\(groupMember.alias[Column.rowID])) AS \(ViewModel.closedGroupUserCountKey) + COUNT(\(groupMember[.rowId])) AS \(ClosedGroupUserCount.Columns.closedGroupUserCount) FROM \(GroupMember.self) WHERE ( \(SQL("\(groupMember[.groupId]) = \(threadId)")) AND \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) ) - ) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)")) + ) AS \(closedGroupUserCount) ON \(SQL("\(closedGroupUserCount[.groupId]) = \(threadId)")) WHERE \(SQL("\(thread[.id]) = \(threadId)")) """ @@ -976,9 +1020,9 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.disappearingMessagesConfigurationString: adapters[1], - ViewModel.contactProfileString: adapters[2] + return ScopeAdapter.with(ViewModel.self, [ + .disappearingMessagesConfiguration: adapters[1], + .contactProfile: adapters[2] ]) } } @@ -986,39 +1030,40 @@ public extension SessionThreadViewModel { static func conversationSettingsQuery(threadId: String, userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 9 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), EXISTS ( SELECT 1 @@ -1028,7 +1073,7 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), EXISTS ( SELECT 1 @@ -1038,24 +1083,24 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupAdmin), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), - \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), - \(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.server]) AS \(ViewModel.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(ViewModel.Columns.openGroupRoomToken), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.openGroupPublicKey), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1066,9 +1111,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1079,10 +1124,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) WHERE \(SQL("\(thread[.id]) = \(threadId)")) @@ -1097,11 +1142,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1188,13 +1233,15 @@ public extension SessionThreadViewModel { static func messagesQuery(userPublicKey: String, pattern: FTS5Pattern) -> AdaptedFetchRequest> { let interaction: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() + let profile: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() - let profile: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let interactionLiteral: SQL = SQL(stringLiteral: Interaction.databaseTableName) - let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) + let interactionFullTextSearch: TypedTableAlias = TypedTableAlias(name: Interaction.fullTextSearchTableName) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to @@ -1204,44 +1251,44 @@ public extension SessionThreadViewModel { let numColumnsBeforeProfiles: Int = 6 let request: SQLRequest = """ SELECT - \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(interaction[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey), - snippet(\(interactionFullTextSearch), -1, '', '', '...', 6) AS \(ViewModel.interactionBodyKey), + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), + \(interaction[.timestampMs]) AS \(ViewModel.Columns.interactionTimestampMs), + snippet(\(interactionFullTextSearch), -1, '', '', '...', 6) AS \(ViewModel.Columns.interactionBody), \(interaction[.authorId]), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(Interaction.self) JOIN \(interactionFullTextSearch) ON ( - \(interactionFullTextSearch).rowid = \(interactionLiteral).rowid AND - \(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) + \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND + \(interactionFullTextSearch[.body]) MATCH \(pattern) ) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(interaction[.threadId]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(interaction[.threadId]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1252,9 +1299,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1265,10 +1312,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(userPublicKey) ) ORDER BY \(Column.rank), \(interaction[.timestampMs].desc) @@ -1284,11 +1331,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1311,31 +1358,26 @@ public extension SessionThreadViewModel { /// returned results will always be `-1` for those results static func contactsAndGroupsQuery(userPublicKey: String, pattern: FTS5Pattern, searchTerm: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() + let groupMemberProfile: TypedTableAlias = TypedTableAlias(name: "groupMemberProfile") let openGroup: TypedTableAlias = TypedTableAlias() + let groupMemberInfo: TypedTableAlias = TypedTableAlias(name: "groupMemberInfo") let profile: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) + let profileFullTextSearch: TypedTableAlias = TypedTableAlias(name: Profile.fullTextSearchTableName) + let closedGroupFullTextSearch: TypedTableAlias = TypedTableAlias(name: ClosedGroup.fullTextSearchTableName) + let openGroupFullTextSearch: TypedTableAlias = TypedTableAlias(name: OpenGroup.fullTextSearchTableName) - let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) - let closedGroupNameColumnLiteral: SQL = SQL(stringLiteral: ClosedGroup.Columns.name.name) - let closedGroupLiteral: SQL = SQL(stringLiteral: ClosedGroup.databaseTableName) - let closedGroupFullTextSearch: SQL = SQL(stringLiteral: ClosedGroup.fullTextSearchTableName) - let openGroupNameColumnLiteral: SQL = SQL(stringLiteral: OpenGroup.Columns.name.name) - let openGroupLiteral: SQL = SQL(stringLiteral: OpenGroup.databaseTableName) - let openGroupFullTextSearch: SQL = SQL(stringLiteral: OpenGroup.fullTextSearchTableName) - let groupMemberInfoLiteral: SQL = SQL(stringLiteral: "groupMemberInfo") - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) - let groupMemberProfileLiteral: SQL = SQL(stringLiteral: "groupMemberProfile") let noteToSelfLiteral: SQL = SQL(stringLiteral: "NOTE_TO_SELF".localized().lowercased()) let searchTermLiteral: SQL = SQL(stringLiteral: searchTerm.lowercased()) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// We use `IFNULL(rank, 100)` because the custom `Note to Self` like comparison will get a null /// `rank` value which ends up as the first result, by defaulting to `100` it will always be ranked last compared @@ -1346,24 +1388,24 @@ public extension SessionThreadViewModel { SELECT IFNULL(\(Column.rank), 100) AS \(Column.rank), - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), - \(groupMemberInfoLiteral).\(ViewModel.threadMemberNamesKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), + \(groupMemberInfo[.threadMemberNames]), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) @@ -1371,18 +1413,13 @@ public extension SessionThreadViewModel { // MARK: --Contact Threads let contactQueryCommonJoinFilterGroup: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN \(OpenGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND @@ -1394,8 +1431,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += contactQueryCommonJoinFilterGroup @@ -1409,8 +1446,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += contactQueryCommonJoinFilterGroup @@ -1425,14 +1462,14 @@ public extension SessionThreadViewModel { LEFT JOIN ( SELECT \(groupMember[.groupId]), - GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(ViewModel.threadMemberNamesKey) + GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(GroupMemberInfo.Columns.threadMemberNames) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) GROUP BY \(groupMember[.groupId]) - ) AS \(groupMemberInfoLiteral) ON \(groupMemberInfoLiteral).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + ) AS \(groupMemberInfo) ON \(groupMemberInfo[.groupId]) = \(closedGroup[.threadId]) + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1443,9 +1480,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1456,13 +1493,13 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey) + LEFT JOIN \(closedGroupProfileBackFallback) ON ( + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(userPublicKey) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false - LEFT JOIN \(OpenGroup.self) ON false + LEFT JOIN \(contactProfile.never) + LEFT JOIN \(openGroup.never) WHERE ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.legacyGroup)")) OR @@ -1480,8 +1517,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(closedGroupFullTextSearch) ON ( - \(closedGroupFullTextSearch).rowid = \(closedGroupLiteral).rowid AND - \(closedGroupFullTextSearch).\(closedGroupNameColumnLiteral) MATCH \(pattern) + \(closedGroupFullTextSearch[.rowId]) = \(closedGroup[.rowId]) AND + \(closedGroupFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1494,10 +1531,10 @@ public extension SessionThreadViewModel { """ sqlQuery += selectQuery sqlQuery += """ - JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId]) + JOIN \(groupMemberProfile) ON \(groupMemberProfile[.id]) = \(groupMember[.profileId]) JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(groupMemberProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1510,10 +1547,10 @@ public extension SessionThreadViewModel { """ sqlQuery += selectQuery sqlQuery += """ - JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId]) + JOIN \(groupMemberProfile) ON \(groupMemberProfile[.id]) = \(groupMember[.profileId]) JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(groupMemberProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1529,20 +1566,15 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) JOIN \(openGroupFullTextSearch) ON ( - \(openGroupFullTextSearch).rowid = \(openGroupLiteral).rowid AND - \(openGroupFullTextSearch).\(openGroupNameColumnLiteral) MATCH \(pattern) + \(openGroupFullTextSearch[.rowId]) = \(openGroup[.rowId]) AND + \(openGroupFullTextSearch[.name]) MATCH \(pattern) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + LEFT JOIN \(contactProfile.never) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND @@ -1552,18 +1584,13 @@ public extension SessionThreadViewModel { // MARK: --Note to Self Thread let noteToSelfQueryCommonJoins: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(OpenGroup.self) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(groupMemberInfo.never) """ // Note to self thread searching for 'Note to Self' (need to join an FTS table to @@ -1596,8 +1623,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += noteToSelfQueryCommonJoins @@ -1616,8 +1643,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += noteToSelfQueryCommonJoins @@ -1631,41 +1658,36 @@ public extension SessionThreadViewModel { SELECT IFNULL(\(Column.rank), 100) AS \(Column.rank), - -1 AS \(ViewModel.rowIdKey), - \(contact[.id]) AS \(ViewModel.threadIdKey), - \(SQL("\(SessionThread.Variant.contact)")) AS \(ViewModel.threadVariantKey), - 0 AS \(ViewModel.threadCreationDateTimestampKey), - \(groupMemberInfoLiteral).\(ViewModel.threadMemberNamesKey), + -1 AS \(ViewModel.Columns.rowId), + \(contact[.id]) AS \(ViewModel.Columns.threadId), + \(SQL("\(SessionThread.Variant.contact)")) AS \(ViewModel.Columns.threadVariant), + 0 AS \(ViewModel.Columns.threadCreationDateTimestamp), + \(groupMemberInfo[.threadMemberNames]), - false AS \(ViewModel.threadIsNoteToSelfKey), - -1 AS \(ViewModel.threadPinnedPriorityKey), + false AS \(ViewModel.Columns.threadIsNoteToSelf), + -1 AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(Contact.self) """ let hiddenContactQueryCommonJoins: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(contact[.id]) + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(contact[.id]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(contact[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN \(OpenGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(thread[.id]) IS NULL GROUP BY \(contact[.id]) @@ -1681,8 +1703,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += hiddenContactQueryCommonJoins @@ -1697,8 +1719,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += hiddenContactQueryCommonJoins @@ -1713,13 +1735,13 @@ public extension SessionThreadViewModel { \(sqlQuery) ) - GROUP BY \(ViewModel.threadIdKey) + GROUP BY \(ViewModel.Columns.threadId) ORDER BY \(Column.rank), - \(ViewModel.threadIsNoteToSelfKey), - \(ViewModel.closedGroupNameKey), - \(ViewModel.openGroupNameKey), - \(ViewModel.threadIdKey) + \(ViewModel.Columns.threadIsNoteToSelf), + \(ViewModel.Columns.closedGroupName), + \(ViewModel.Columns.openGroupName), + \(ViewModel.Columns.threadId) LIMIT \(SQL("\(SessionThreadViewModel.searchResultsLimit)")) """ @@ -1748,11 +1770,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1760,31 +1782,30 @@ public extension SessionThreadViewModel { /// This method returns only the 'Note to Self' thread in the structure of a search result conversation static func noteToSelfOnlyQuery(userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw let numColumnsBeforeProfiles: Int = 8 let request: SQLRequest = """ SELECT 100 AS \(Column.rank), - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), - '' AS \(ViewModel.threadMemberNamesKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), + '' AS \(ViewModel.Columns.threadMemberNames), - true AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + true AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, + \(contactProfile.allColumns), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) WHERE \(SQL("\(thread[.id]) = \(userPublicKey)")) """ @@ -1797,8 +1818,8 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1] ]) } } @@ -1810,67 +1831,90 @@ public extension SessionThreadViewModel { static func shareQuery(userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results - let numColumnsBeforeProfiles: Int = 7 + let numColumnsBeforeProfiles: Int = 8 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + ( + \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND + \(SQL("\(thread[.id]) != \(userPublicKey)")) AND + IFNULL(\(contact[.isApproved]), false) = false + ) AS \(ViewModel.Columns.threadIsMessageRequest), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + EXISTS ( + SELECT 1 + FROM \(GroupMember.self) + WHERE ( + \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND + \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND + \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) + ) + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), + + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), + \(openGroup[.permissions]) AS \(ViewModel.Columns.openGroupPermissions), + + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), + + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + 0 AS \(AggregateInteraction.Columns.threadUnreadCount), + 0 AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) LEFT JOIN \(Interaction.self) ON ( \(interaction[.threadId]) = \(thread[.id]) AND - \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey) + \(interaction[.id]) = \(aggregateInteraction[.interactionId]) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1881,9 +1925,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1894,10 +1938,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) WHERE ( @@ -1925,11 +1969,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } 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 7266b30f6..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, @@ -509,19 +510,23 @@ public struct ProfileManager { // Name if let name: String = name, !name.isEmpty, name != profile.name { - // FIXME: Remove the `userConfigsEnabled` check once `useSharedUtilForUserConfig` is permanent - if sentTimestamp > profile.lastNameUpdate || (isCurrentUser && (calledFromConfigHandling || !SessionUtil.userConfigsEnabled(db))) { + if sentTimestamp > profile.lastNameUpdate || (isCurrentUser && calledFromConfigHandling) { profileChanges.append(Profile.Columns.name.set(to: name)) profileChanges.append(Profile.Columns.lastNameUpdate.set(to: sentTimestamp)) } } + // 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 - // FIXME: Remove the `userConfigsEnabled` check once `useSharedUtilForUserConfig` is permanent - if sentTimestamp > profile.lastProfilePictureUpdate || (isCurrentUser && (calledFromConfigHandling || !SessionUtil.userConfigsEnabled(db))) { + if sentTimestamp > profile.lastProfilePictureUpdate || (isCurrentUser && calledFromConfigHandling) { switch avatarUpdate { case .none: break case .uploadImageData: preconditionFailure("Invalid options for this function") @@ -571,25 +576,6 @@ public struct ProfileManager { profileChanges ) } - // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - else if !SessionUtil.userConfigsEnabled(db) { - // If we have a contact record for the profile (ie. it's a synced profile) then - // should should send an updated config message, otherwise we should just update - // the local state (the shared util has this logic build in to it's handling) - if (try? Contact.exists(db, id: publicKey)) == true { - try Profile - .filter(id: publicKey) - .updateAllAndConfig(db, profileChanges) - } - else { - try Profile - .filter(id: publicKey) - .updateAll( - db, - profileChanges - ) - } - } else { try Profile .filter(id: publicKey) 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/SessionShareExtension/ThreadPickerViewModel.swift b/SessionShareExtension/ThreadPickerViewModel.swift index 2d07a43cd..e2bae7488 100644 --- a/SessionShareExtension/ThreadPickerViewModel.swift +++ b/SessionShareExtension/ThreadPickerViewModel.swift @@ -28,6 +28,7 @@ public class ThreadPickerViewModel { .shareQuery(userPublicKey: userPublicKey) .fetchAll(db) } + .map { threads -> [SessionThreadViewModel] in threads.filter { $0.canWrite } } // Exclude unwritable threads .removeDuplicates() .handleEvents(didFail: { SNLog("[ThreadPickerViewModel] Observation failed with error: \($0)") }) 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 cd76ace82..2278e4670 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,13 +236,24 @@ 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 SUKLegacy.clearLegacyDatabaseInstance() + // 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 // launch so is unnecessarily alarming, it also gets caught and logged separately by @@ -283,6 +303,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, @@ -300,7 +336,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 b8fbfdf73..9932f4719 100644 --- a/SessionUtilitiesKit/Database/StorageError.swift +++ b/SessionUtilitiesKit/Database/StorageError.swift @@ -8,6 +8,8 @@ public enum StorageError: Error { case startupFailed case migrationFailed 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/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index ab6ae915f..2c41643c2 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -1351,18 +1351,24 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet // Fetch the inserted/updated rows let additionalFilters: SQL = SQL(rowIds.contains(Column.rowID)) - let updatedItems: [T] = (try? dataQuery(additionalFilters) - .fetchAll(db)) - .defaulting(to: []) - // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link - // preview) then trigger the update callback (if there were deletions) and stop here - guard !updatedItems.isEmpty else { return hasOtherChanges } - - // Process the upserted data (assume at least one value changed) - dataCache.mutate { $0 = $0.upserting(items: updatedItems) } - - return true + do { + let updatedItems: [T] = try dataQuery(additionalFilters) + .fetchAll(db) + + // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link + // preview) then trigger the update callback (if there were deletions) and stop here + guard !updatedItems.isEmpty else { return hasOtherChanges } + + // Process the upserted data (assume at least one value changed) + dataCache.mutate { $0 = $0.upserting(items: updatedItems) } + + return true + } + catch { + SNLog("[PagedDatabaseObserver] Error loading associated data: \(error)") + return hasOtherChanges + } } public func clearCache(_ db: Database) { diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift index 14dd0aacd..ed42d63e0 100644 --- a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift +++ b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift @@ -3,22 +3,65 @@ import Foundation import GRDB -public class TypedTableAlias where T: TableRecord, T: ColumnExpressible { - public let alias: TableAlias = TableAlias(name: T.databaseTableName) +public struct TypedTableAlias { + public enum RowIdColumn { + case rowId + } - public init() {} + internal let name: String + internal let tableName: String? + internal let alias: TableAlias + + public var allColumns: SQLSelection { alias[AllColumns().sqlSelection] } + public var never: NeverJoiningTypedTableAlias { NeverJoiningTypedTableAlias(alias: self) } + + // MARK: - Initialization + + public init(name: String, tableName: String? = nil) { + self.name = name + self.tableName = tableName + self.alias = TableAlias(name: name) + } + + public init(name: String) where T: TableRecord { + self.name = name + self.tableName = T.databaseTableName + self.alias = TableAlias(name: name) + } + + public init() where T: TableRecord { + self = TypedTableAlias(name: T.databaseTableName) + } + + public init(_ viewModel: VM.Type, column: VM.Columns, tableName: String?) { + self.name = column.name + self.tableName = tableName + self.alias = TableAlias(name: name) + } + + public init(_ viewModel: VM.Type, column: VM.Columns) where T: TableRecord { + self = TypedTableAlias(viewModel, column: column, tableName: T.databaseTableName) + } + + // MARK: - Functions public subscript(_ column: T.Columns) -> SQLExpression { return alias[column.name] } - /// **Warning:** For this to work you **MUST** call the '.aliased()' method when joining or it will - /// throw when trying to decode - public func allColumns() -> SQLSelection { - return alias[AllColumns().sqlSelection] + public subscript(_ column: RowIdColumn) -> SQLSelection { + return alias[Column.rowID] } } +// MARK: - NeverJoiningTypedTableAlias + +public struct NeverJoiningTypedTableAlias { + internal let alias: TypedTableAlias +} + +// MARK: - Extensions + extension QueryInterfaceRequest { public func aliased(_ typedAlias: TypedTableAlias) -> Self { return aliased(typedAlias.alias) @@ -32,7 +75,5 @@ extension Association { } extension TableAlias { - public func allColumns() -> SQLSelection { - return self[AllColumns().sqlSelection] - } + public var allColumns: SQLSelection { self[AllColumns().sqlSelection] } } diff --git a/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift new file mode 100644 index 000000000..01d0c64bb --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift @@ -0,0 +1,69 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension SQLInterpolation { + /// Appends the table name of the record type. + /// + /// // SELECT * FROM player + /// let player: TypedTableAlias = TypedTableAlias() + /// let request: SQLRequest = "SELECT * FROM \(player)" + @_disfavoredOverload + mutating func appendInterpolation(_ typedTableAlias: TypedTableAlias) { + let name: String = typedTableAlias.name + + guard let tableName: String = typedTableAlias.tableName else { return appendLiteral(name.quotedDatabaseIdentifier) } + guard name != tableName else { return appendLiteral(tableName.quotedDatabaseIdentifier) } + + appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(name.quotedDatabaseIdentifier)") + } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN team AS testTeam ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let testTeam: TypedTableAlias = TypedTableAlias(name: "testTeam") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(testTeam.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) where T: TableRecord { + guard let tableName: String = neverJoiningAlias.alias.tableName else { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + return + } + + appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN (SELECT 0 AS teamInfo.Column.A, 0 AS teamInfo.Column.B) AS teamInfo ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let teamInfo: TypedTableAlias = TypedTableAlias(name: "teamInfo") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) where T.Columns: CaseIterable { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN (SELECT 0 AS teamInfo.Column.A, 0 AS teamInfo.Column.B) AS teamInfo ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let teamInfo: TypedTableAlias = TypedTableAlias(name: "teamInfo") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + private func generateSelection(for type: T.Type) -> String where T.Columns: CaseIterable { + return T.Columns.allCases + .map { "NULL AS \($0.name)" } + .joined(separator: ", ") + } + + private func generateSelection(for type: T.Type) -> String { + return "SELECT 1" + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift new file mode 100644 index 000000000..500131972 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension ScopeAdapter { + static func with( + _ viewModel: VM.Type, + _ scopes: [VM.Columns: RowAdapter] + ) -> ScopeAdapter { + return ScopeAdapter(scopes.reduce(into: [:]) { result, next in result[next.key.name] = next.value }) + } +} 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 bc55e8231..a1829aab0 100644 --- a/SessionUtilitiesKit/General/SNUserDefaults.swift +++ b/SessionUtilitiesKit/General/SNUserDefaults.swift @@ -37,7 +37,6 @@ public enum SNUserDefaults { } public enum Date: Swift.String { - case lastConfigurationSync case lastProfilePictureUpload case lastOpenGroupImageUpdate case lastOpen @@ -62,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 58f0c6856..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,12 +107,8 @@ public enum AppSetup { ) } - // Refresh the migration state for 'SessionUtil' so it's logic can start running - // correctly when called (doing this here instead of automatically via the - // `SessionUtil.userConfigsEnabled` property to avoid having to use the correct - // method when calling within a database read/write closure) - Storage.shared.read { db in SessionUtil.refreshingUserConfigsEnabled(db) } - + // 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 {