diff --git a/.gitmodules b/.gitmodules index b8c1e380954..94e414a92da 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "LibSession-Util"] path = LibSession-Util - url = https://github.com/oxen-io/libsession-util.git + url = https://github.com/session-foundation/libsession-util.git diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2e50a323b87..1aae439d162 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -685,6 +685,10 @@ 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 */; }; + FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; + FD6C67262CF6EA2300B350A7 /* PushRegistrationManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67252CF6EA1E00B350A7 /* PushRegistrationManagerType.swift */; }; + FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; + FD6DA9D22D0160F10092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9D12D0160F10092085A /* Lucide */; }; FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; @@ -732,6 +736,9 @@ FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */; }; FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD72BD9A2BDF5EEA00CF6CF6 /* Message+Origin.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BD992BDF5EEA00CF6CF6 /* Message+Origin.swift */; }; + FD756BEB2D0181D700BD7199 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FD756BEA2D0181D700BD7199 /* GRDB */; }; + FD756BF02D06686500BD7199 /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD756BEF2D06686500BD7199 /* Lucide */; }; + FD756BF22D06687800BD7199 /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD756BF12D06687800BD7199 /* Lucide */; }; FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; }; FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; }; FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; @@ -840,7 +847,9 @@ 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 */; }; - FDC289422C86AB5800020BC2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FDC289412C86AB5800020BC2 /* GRDB */; }; + FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD652CFD6C4E002CDC71 /* Config.swift */; }; + FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */; }; + FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */; }; FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */; }; FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; @@ -1832,6 +1841,8 @@ 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 = ""; }; + FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; + FD6C67252CF6EA1E00B350A7 /* PushRegistrationManagerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRegistrationManagerType.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddSnodeReveivedMessageInfoPrimaryKey.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 = ""; }; @@ -1962,6 +1973,9 @@ 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 = ""; }; + FDC1BD652CFD6C4E002CDC71 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsViewModel.swift; sourceTree = ""; }; + FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryArchiver.swift; sourceTree = ""; }; FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableIdentifiable.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 = ""; }; @@ -2160,6 +2174,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FD756BF22D06687800BD7199 /* Lucide in Frameworks */, FD2286712C38D43000BC06F7 /* DifferenceKit in Frameworks */, FD6A396B2C2D284500762359 /* YYImage in Frameworks */, FD37E9EF28A5ED70003AE748 /* SessionUtilitiesKit.framework in Frameworks */, @@ -2198,7 +2213,7 @@ FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */, FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */, FD6A396D2C2D284B00762359 /* YYImage in Frameworks */, - FDC289422C86AB5800020BC2 /* GRDB in Frameworks */, + FD756BEB2D0181D700BD7199 /* GRDB in Frameworks */, FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2228,7 +2243,9 @@ 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */, 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */, 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */, + FD756BF02D06686500BD7199 /* Lucide in Frameworks */, 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */, + FD6DA9D22D0160F10092085A /* Lucide in Frameworks */, B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */, FDEF572A2C3CF50B00131302 /* WebRTC in Frameworks */, 45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */, @@ -2244,6 +2261,7 @@ A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */, A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */, A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */, + FD6DA9CF2D015B440092085A /* Lucide in Frameworks */, A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */, D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */, C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */, @@ -2783,6 +2801,7 @@ FD716E692850327900C96BF4 /* EndCallMode.swift */, FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */, FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */, + FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */, ); path = Calls; sourceTree = ""; @@ -3101,6 +3120,7 @@ FD37E9CB28A1E578003AE748 /* AppearanceViewController.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, + FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); @@ -3177,6 +3197,7 @@ children = ( FDC498B52AC15F6D00EDD897 /* Types */, 4539B5851F79348F007141FF /* PushRegistrationManager.swift */, + FD6C67252CF6EA1E00B350A7 /* PushRegistrationManagerType.swift */, 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */, 451A13B01E13DED2000A50FD /* AppNotifications.swift */, 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */, @@ -3614,6 +3635,7 @@ FD6A39272C2AB2AA00762359 /* CGSize+Utilities.swift */, FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, FD12A84A2AD6458800EEBA0D /* DifferenceKit+Utilities.swift */, + FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */, FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */, FD09796A27F6C67500936362 /* Failable.swift */, FD6A39092C2A8F2D00762359 /* FileManagerType.swift */, @@ -4162,8 +4184,9 @@ FD8ECF7529340F4800C0D1BB /* LibSession */ = { isa = PBXGroup; children = ( - FD2B4B022949886900AB4848 /* Database */, FD8ECF8E29381FB200C0D1BB /* Config Handling */, + FD2B4B022949886900AB4848 /* Database */, + FDC1BD642CFD6C44002CDC71 /* Types */, FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */, ); path = LibSession; @@ -4248,6 +4271,14 @@ path = Types; sourceTree = ""; }; + FDC1BD642CFD6C44002CDC71 /* Types */ = { + isa = PBXGroup; + children = ( + FDC1BD652CFD6C4E002CDC71 /* Config.swift */, + ); + path = Types; + sourceTree = ""; + }; FDC289482C881C5500020BC2 /* LibSession */ = { isa = PBXGroup; children = ( @@ -4671,6 +4702,7 @@ packageProductDependencies = ( FD6A396A2C2D284500762359 /* YYImage */, FD2286702C38D43000BC06F7 /* DifferenceKit */, + FD756BF12D06687800BD7199 /* Lucide */, ); productName = SessionUIKit; productReference = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; @@ -4739,7 +4771,7 @@ FD6A38EE2C2A641200762359 /* DifferenceKit */, FD6A39652C2D21E400762359 /* libwebp */, FD6A396C2C2D284B00762359 /* YYImage */, - FDC289412C86AB5800020BC2 /* GRDB */, + FD756BEA2D0181D700BD7199 /* GRDB */, ); productName = SessionUtilities; productReference = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; @@ -4802,6 +4834,9 @@ FD6A39682C2D283A00762359 /* YYImage */, FD2286782C38D4FF00BC06F7 /* DifferenceKit */, FDEF57292C3CF50B00131302 /* WebRTC */, + FD6DA9CE2D015B440092085A /* Lucide */, + FD6DA9D12D0160F10092085A /* Lucide */, + FD756BEF2D06686500BD7199 /* Lucide */, ); productName = RedPhone; productReference = D221A089169C9E5E00537ABF /* Session.app */; @@ -5048,7 +5083,8 @@ FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */, FD6A39642C2D21E400762359 /* XCRemoteSwiftPackageReference "libwebp-Xcode" */, FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */, - FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */, + FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */, + FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */, ); productRefGroup = D221A08A169C9E5E00537ABF /* Products */; projectDirPath = ""; @@ -5815,6 +5851,7 @@ FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */, FD428B212B4B75EA006D0888 /* Singleton.swift in Sources */, FD6A39082C2A8DDA00762359 /* FileSystem.swift in Sources */, + FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, FD8ECF922938552800C0D1BB /* Threading.swift in Sources */, FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */, @@ -5929,6 +5966,7 @@ FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, + FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */, FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */, @@ -5990,6 +6028,7 @@ FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, + FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */, FD245C53285065DB00B966DD /* ProximityMonitoringManager.swift in Sources */, FD245C55285065E500B966DD /* OpenGroupManager.swift in Sources */, C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, @@ -6226,6 +6265,7 @@ FDEF57242C3CF04700131302 /* WebRTC+Utilities.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */, + FD6C67262CF6EA2300B350A7 /* PushRegistrationManagerType.swift in Sources */, B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, @@ -6243,6 +6283,7 @@ B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */, FD12A8472AD63C3400EEBA0D /* PagedObservationSource.swift in Sources */, B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */, + FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */, 7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */, C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */, 942256822C23F8BB00C0FDBF /* InviteAFriendScreen.swift in Sources */, @@ -7636,7 +7677,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 507; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7673,7 +7714,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.1; + MARKETING_VERSION = 2.8.3; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -7715,7 +7756,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 507; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -7747,7 +7788,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.1; + MARKETING_VERSION = 2.8.3; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -7778,7 +7819,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 496; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7816,7 +7856,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.2; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7849,7 +7888,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 496; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7887,7 +7925,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.2; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; @@ -8630,18 +8667,26 @@ }; FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/oxen-io/session-ios-yyimage.git"; + repositoryURL = "https://github.com/session-foundation/session-ios-yyimage"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.1.0; }; }; - FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { + FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/oxen-io/session-grdb-swift.git"; + repositoryURL = "https://github.com/session-foundation/session-grdb-swift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 106.29.2; + minimumVersion = 106.29.3; + }; + }; + FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/session-foundation/session-lucide.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.468.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -8775,11 +8820,29 @@ package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */; productName = YYImage; }; - FDC289412C86AB5800020BC2 /* GRDB */ = { + FD6DA9CE2D015B440092085A /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + productName = Lucide; + }; + FD6DA9D12D0160F10092085A /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + productName = Lucide; + }; + FD756BEA2D0181D700BD7199 /* GRDB */ = { isa = XCSwiftPackageProductDependency; - package = FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */; + package = FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */; productName = GRDB; }; + FD756BEF2D06686500BD7199 /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + package = FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */; + productName = Lucide; + }; + FD756BF12D06687800BD7199 /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + package = FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */; + productName = Lucide; + }; FDEF57292C3CF50B00131302 /* WebRTC */ = { isa = XCSwiftPackageProductDependency; package = FD6A390E2C2A93CD00762359 /* XCRemoteSwiftPackageReference "WebRTC" */; diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 97979c57d34..70222e3b912 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "53c1182a7d64df9a13a5c0ec4420b2c8da9c4d81d97b2b79b6e8f2946978471b", + "originHash" : "c57241b796915b0642f9c260463b2d6fd7d5198beafde785c590f3a7d80d31f5", "pins" : [ { "identity" : "cocoalumberjack", @@ -85,21 +85,30 @@ { "identity" : "session-grdb-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/oxen-io/session-grdb-swift.git", + "location" : "https://github.com/session-foundation/session-grdb-swift.git", "state" : { - "revision" : "04f480b95263b7c517085100ceb249f879c021d8", + "revision" : "b3643613f1e0f392fa41072ee499da93b4c06b67", "version" : "106.29.3" } }, { "identity" : "session-ios-yyimage", "kind" : "remoteSourceControl", - "location" : "https://github.com/oxen-io/session-ios-yyimage.git", + "location" : "https://github.com/session-foundation/session-ios-yyimage", "state" : { "revision" : "14786afd2523f80be304b377f9dbab6b7904bf02", "version" : "1.1.0" } }, + { + "identity" : "session-lucide", + "kind" : "remoteSourceControl", + "location" : "https://github.com/session-foundation/session-lucide.git", + "state" : { + "revision" : "32fdd138a20e828eea376d01e53efb461cc19ebe", + "version" : "0.468.0" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 010f9c78a45..5c07c4cd041 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -15,10 +15,12 @@ import SessionSnodeKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { @objc static let isEnabled = true + private let dependencies: Dependencies + // MARK: - Metadata Properties public let uuid: String public let callId: UUID // This is for CallKit - let sessionId: String + public let sessionId: String let mode: CallMode var audioMode: AudioMode public let webRTCSession: WebRTCSession @@ -147,7 +149,8 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // MARK: - Initialization - init(_ db: Database, for sessionId: String, uuid: String, mode: CallMode, outgoing: Bool = false) { + init(_ db: Database, for sessionId: String, uuid: String, mode: CallMode, outgoing: Bool = false, using dependencies: Dependencies) { + self.dependencies = dependencies self.sessionId = sessionId self.uuid = uuid self.callId = UUID() @@ -172,8 +175,8 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { WebRTCSession.current = self.webRTCSession self.webRTCSession.delegate = self - if AppEnvironment.shared.callManager.currentCall == nil { - AppEnvironment.shared.callManager.currentCall = self + if Singleton.callManager.currentCall == nil { + Singleton.callManager.setCurrentCall(self) } else { SNLog("[Calls] A call is ongoing.") @@ -183,12 +186,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // stringlint:ignore_contents func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) { guard case .answer = mode else { - SessionCallManager.reportFakeCall(info: "Call not in answer mode") + SessionCallManager.reportFakeCall(info: "Call not in answer mode", using: dependencies) return } setupTimeoutTimer() - AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in + Singleton.callManager.reportIncomingCall(self, callerName: contactName) { error in completion(error) } } @@ -290,7 +293,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // MARK: - Call Message Handling - public func updateCallMessage(mode: EndCallMode) { + public func updateCallMessage(mode: EndCallMode, using dependencies: Dependencies) { guard let callInteractionId: Int64 = callInteractionId else { return } let duration: TimeInterval = self.duration @@ -351,11 +354,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { threadId: interaction.threadId, threadVariant: threadVariant, includingOlder: false, - trySendReadReceipt: false + trySendReadReceipt: false, + using: dependencies ) }, completion: { _, _ in - SessionCallManager.suspendDatabaseIfCallEndedInBackground() + Singleton.callManager.suspendDatabaseIfCallEndedInBackground() } ) } @@ -404,7 +408,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { guard Singleton.hasAppContext else { return } if let callVC = Singleton.appContext.frontmostViewController as? CallVC { callVC.handleEndCallMessage() } if let miniCallView = MiniCallView.current { miniCallView.dismiss() } - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: .remoteEnded) + Singleton.callManager.reportCurrentCallEnded(reason: .remoteEnded) } } @@ -461,7 +465,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { timeOutTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: timeInterval, repeats: false) { _ in self.didTimeout = true - AppEnvironment.shared.callManager.endCall(self) { error in + Singleton.callManager.endCall(self) { error in self.timeOutTimer = nil } } diff --git a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift index 1824a9d5c16..ad078c5251b 100644 --- a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift +++ b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift @@ -2,12 +2,16 @@ import Foundation import CallKit +import SessionMessagingKit import SessionUtilitiesKit extension SessionCallManager { - public func startCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - guard case .offer = call.mode else { return } - guard !call.hasConnected else { return } + public func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + guard + let call: SessionCall = call as? SessionCall, + case .offer = call.mode, + !call.hasConnected + else { return } reportOutgoingCall(call) @@ -28,9 +32,9 @@ extension SessionCallManager { } } - public func answerCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - if callController != nil { - let answerCallAction = CXAnswerCallAction(call: call.callId) + public func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + if callController != nil, let callId: UUID = call?.callId { + let answerCallAction = CXAnswerCallAction(call: callId) let transaction = CXTransaction() transaction.addAction(answerCallAction) @@ -42,9 +46,9 @@ extension SessionCallManager { } } - public func endCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - if callController != nil { - let endCallAction = CXEndCallAction(call: call.callId) + public func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + if callController != nil, let callId: UUID = call?.callId { + let endCallAction = CXEndCallAction(call: callId) let transaction = CXTransaction() transaction.addAction(endCallAction) diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 7235ec02aed..51173ef2c7d 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -9,7 +9,21 @@ import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit +// MARK: - Cache + +public extension Cache { + static let callManager: CacheInfo.Config = CacheInfo.create( + createInstance: { SessionCallManager.Cache() }, + mutableInstance: { $0 }, + immutableInstance: { $0 } + ) +} + +// MARK: - SessionCallManager + public final class SessionCallManager: NSObject, CallManagerProtocol { + let dependencies: Dependencies + let provider: CXProvider? let callController: CXCallController? @@ -27,40 +41,15 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } - private static var _sharedProvider: CXProvider? - static func sharedProvider(useSystemCallLog: Bool) -> CXProvider { - let configuration = buildProviderConfiguration(useSystemCallLog: useSystemCallLog) - - if let sharedProvider = self._sharedProvider { - sharedProvider.configuration = configuration - return sharedProvider - } - else { - SwiftSingletons.register(self) - let provider = CXProvider(configuration: configuration) - _sharedProvider = provider - return provider - } - } - - static func buildProviderConfiguration(useSystemCallLog: Bool) -> CXProviderConfiguration { - let providerConfiguration = CXProviderConfiguration() - providerConfiguration.supportsVideo = true - providerConfiguration.maximumCallGroups = 1 - providerConfiguration.maximumCallsPerCallGroup = 1 - providerConfiguration.supportedHandleTypes = [.generic] - let iconMaskImage = #imageLiteral(resourceName: "SessionGreen32") - providerConfiguration.iconTemplateImageData = iconMaskImage.pngData() - providerConfiguration.includesCallsInRecents = useSystemCallLog - - return providerConfiguration - } - // MARK: - Initialization - init(useSystemCallLog: Bool = false) { + init(useSystemCallLog: Bool = false, using dependencies: Dependencies) { + self.dependencies = dependencies + if Preferences.isCallKitSupported { - self.provider = SessionCallManager.sharedProvider(useSystemCallLog: useSystemCallLog) + self.provider = dependencies.caches.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: useSystemCallLog) + } self.callController = CXCallController() } else { @@ -76,9 +65,11 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - Report calls - public static func reportFakeCall(info: String) { + public static func reportFakeCall(info: String, using dependencies: Dependencies) { let callId = UUID() - let provider = SessionCallManager.sharedProvider(useSystemCallLog: false) + let provider: CXProvider = dependencies.caches.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: false) + } provider.reportNewIncomingCall( with: callId, update: CXCallUpdate() @@ -92,6 +83,10 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { ) } + public func setCurrentCall(_ call: CurrentCallProtocol?) { + self.currentCall = call + } + public func reportOutgoingCall(_ call: SessionCall) { Log.assertOnMainThread() UserDefaults.sharedLokiProject?[.isCallOngoing] = true @@ -108,12 +103,14 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } - public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) { - let provider = provider ?? Self.sharedProvider(useSystemCallLog: false) + public func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) { + let provider: CXProvider = dependencies.caches.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: false) + } // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.localizedCallerName = callerName - update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) + update.remoteHandle = CXHandle(type: .generic, value: call.sessionId) update.hasVideo = false disableUnsupportedFeatures(callUpdate: update) @@ -140,11 +137,12 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } func handleCallEnded() { + SNLog("[Calls] Call ended.") WebRTCSession.current = nil UserDefaults.sharedLokiProject?[.isCallOngoing] = false UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil - if Singleton.hasAppContext && Singleton.appContext.isInBackground { + if Singleton.hasAppContext && Singleton.appContext.isNotInForeground { (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() Log.flush() } @@ -152,7 +150,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { guard let call = currentCall else { handleCallEnded() - Self.suspendDatabaseIfCallEndedInBackground() + suspendDatabaseIfCallEndedInBackground() return } @@ -160,14 +158,14 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { self.provider?.reportCall(with: call.callId, endedAt: nil, reason: reason) switch (reason) { - case .answeredElsewhere: call.updateCallMessage(mode: .answeredElsewhere) - case .unanswered: call.updateCallMessage(mode: .unanswered) - case .declinedElsewhere: call.updateCallMessage(mode: .local) - default: call.updateCallMessage(mode: .remote) + case .answeredElsewhere: call.updateCallMessage(mode: .answeredElsewhere, using: dependencies) + case .unanswered: call.updateCallMessage(mode: .unanswered, using: dependencies) + case .declinedElsewhere: call.updateCallMessage(mode: .local, using: dependencies) + default: call.updateCallMessage(mode: .remote, using: dependencies) } } else { - call.updateCallMessage(mode: .local) + call.updateCallMessage(mode: .local, using: dependencies) } (call as? SessionCall)?.webRTCSession.dropConnection() @@ -197,7 +195,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { callUpdate.supportsDTMF = false } - public static func suspendDatabaseIfCallEndedInBackground() { + public func suspendDatabaseIfCallEndedInBackground() { + SNLog("[Calls] suspendDatabaseIfCallEndedInBackground.") if Singleton.hasAppContext && Singleton.appContext.isInBackground { // FIXME: Initialise the `SessionCallManager` with a dependencies instance let dependencies: Dependencies = Dependencies() @@ -214,9 +213,11 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - UI public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) { - guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else { - return - } + guard + let call: SessionCall = Storage.shared.read({ [dependencies] db in + SessionCall(db, for: caller, uuid: uuid, mode: mode, using: dependencies) + }) + else { return } call.callInteractionId = interactionId call.reportIncomingCallIfNeeded { error in @@ -238,8 +239,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { { let callVC = CallVC(for: call) callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 + conversationVC.hideInputAccessoryView() presentingVC.present(callVC, animated: true, completion: nil) } else if !Preferences.isCallKitSupported { @@ -295,3 +295,38 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } +// MARK: - SessionCallManager Cache + +public extension SessionCallManager { + class Cache: CallManagerCacheType { + public var provider: CXProvider? + + public func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider { + if let provider: CXProvider = self.provider { + return provider + } + + let iconMaskImage: UIImage = #imageLiteral(resourceName: "SessionGreen32") + let configuration = CXProviderConfiguration() + configuration.supportsVideo = true + configuration.maximumCallGroups = 1 + configuration.maximumCallsPerCallGroup = 1 + configuration.supportedHandleTypes = [.generic] + configuration.iconTemplateImageData = iconMaskImage.pngData() + configuration.includesCallsInRecents = useSystemCallLog + + let provider: CXProvider = CXProvider(configuration: configuration) + self.provider = provider + return provider + } + } +} + +// MARK: - OGMCacheType + +/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way +public protocol CallManagerImmutableCacheType: ImmutableCacheType {} + +public protocol CallManagerCacheType: CallManagerImmutableCacheType, MutableCacheType { + func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider +} diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index b6b35276757..dfb98f7cbfc 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -429,7 +429,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { _ = call.videoCapturer // Force the lazy var to instantiate titleLabel.text = self.call.contactName - AppEnvironment.shared.callManager.startCall(call) { [weak self] error in + Singleton.callManager.startCall(call) { [weak self] error in DispatchQueue.main.async { if let _ = error { self?.callInfoLabel.text = "callsErrorStart".localized() @@ -600,13 +600,16 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { [weak self] _ in - self?.conversationVC?.showInputAccessoryView() - self?.presentingViewController?.dismiss(animated: true, completion: nil) + DispatchQueue.main.async { + self?.dismiss(animated: true, completion: { + self?.conversationVC?.showInputAccessoryView() + }) + } } } @objc private func answerCall() { - AppEnvironment.shared.callManager.answerCall(call) { [weak self] error in + Singleton.callManager.answerCall(call) { [weak self] error in DispatchQueue.main.async { if let _ = error { self?.callInfoLabel.text = "callsErrorAnswer".localized() @@ -617,15 +620,19 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } @objc private func endCall() { - AppEnvironment.shared.callManager.endCall(call) { [weak self] error in + Singleton.callManager.endCall(call) { [weak self] error in if let _ = error { self?.call.endSessionCall() - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) + Singleton.callManager.reportCurrentCallEnded(reason: nil) } - DispatchQueue.main.async { - self?.conversationVC?.showInputAccessoryView() - self?.presentingViewController?.dismiss(animated: true, completion: nil) + Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in + DispatchQueue.main.async { + self?.dismiss(animated: true, completion: { + self?.conversationVC?.becomeFirstResponder() + self?.conversationVC?.showInputAccessoryView() + }) + } } } } diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 5e0f7850e79..14738a5e7a1 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -184,10 +184,10 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { } @objc private func endCall() { - AppEnvironment.shared.callManager.endCall(call) { error in + Singleton.callManager.endCall(call) { error in if let _ = error { self.call.endSessionCall() - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) + Singleton.callManager.reportCurrentCallEnded(reason: nil) } self.dismiss() diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 7620dc6a547..0280648070f 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -28,13 +28,27 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate case contacts } - private let contactProfiles: [Profile] = Profile.fetchAllContactProfiles(excludeCurrentUser: true) + private let dependencies: Dependencies + private let contactProfiles: [Profile] private lazy var data: [ArraySection] = [ ArraySection(model: .contacts, elements: contactProfiles) ] private var selectedContacts: Set = [] private var searchText: String = "" - + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.contactProfiles = Profile.fetchAllContactProfiles(excludeCurrentUser: true) + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Components private static let textFieldHeight: CGFloat = 50 @@ -329,7 +343,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate } let selectedContacts = self.selectedContacts let message: String? = (selectedContacts.count > 20 ? "deleteAfterLegacyGroupsGroupCreation".localized() : nil) - ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self, dependencies] _ in MessageSender .createClosedGroup(name: name, members: selectedContacts) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) @@ -358,7 +372,8 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate for: thread.id, variant: thread.variant, dismissing: self?.presentingViewController, - animated: false + animated: false, + using: dependencies ) } ) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ba1669fcb97..10315866397 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -120,12 +120,21 @@ extension ConversationVC: let threadId: String = self.viewModel.threadData.threadId - guard AVAudioSession.sharedInstance().recordPermission == .granted else { return } - guard self.viewModel.threadData.threadVariant == .contact else { return } - guard AppEnvironment.shared.callManager.currentCall == nil else { return } - guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: threadId, uuid: UUID().uuidString.lowercased(), mode: .offer, outgoing: true) }) else { - return - } + guard + AVAudioSession.sharedInstance().recordPermission == .granted, + self.viewModel.threadData.threadVariant == .contact, + Singleton.callManager.currentCall == nil, + let call: SessionCall = Storage.shared.read({ [dependencies = viewModel.dependencies] db in + SessionCall( + db, + for: threadId, + uuid: UUID().uuidString.lowercased(), + mode: .offer, + outgoing: true, + using: dependencies + ) + }) + else { return } let callVC = CallVC(for: call) callVC.conversationVC = self @@ -540,7 +549,8 @@ extension ConversationVC: .updateAllAndConfig( db, SessionThread.Columns.shouldBeVisible.set(to: true), - SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority), + using: dependencies ) } @@ -791,13 +801,23 @@ extension ConversationVC: } func hideInputAccessoryView() { - DispatchQueue.main.async { - self.inputAccessoryView?.isHidden = true - self.inputAccessoryView?.alpha = 0 + guard Thread.isMainThread else { + DispatchQueue.main.async { + self.hideInputAccessoryView() + } + return } + self.inputAccessoryView?.isHidden = true + self.inputAccessoryView?.alpha = 0 } func showInputAccessoryView() { + guard Thread.isMainThread else { + DispatchQueue.main.async { + self.showInputAccessoryView() + } + return + } UIView.animate(withDuration: 0.25, animations: { self.inputAccessoryView?.isHidden = false self.inputAccessoryView?.alpha = 1 @@ -958,7 +978,8 @@ extension ConversationVC: .update( db, sessionId: cellViewModel.threadId, - disappearingMessagesConfig: messageDisappearingConfig + disappearingMessagesConfig: messageDisappearingConfig, + using: dependencies ) } self?.dismiss(animated: true, completion: nil) @@ -1244,7 +1265,9 @@ extension ConversationVC: ) ) - self.present(modal, animated: true) + self.present(modal, animated: true) { [weak self] in + self?.hideInputAccessoryView() + } } func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies) { @@ -1256,9 +1279,15 @@ extension ConversationVC: // FIXME: Add in support for starting a thread with a 'blinded25' id guard (try? SessionId.Prefix(from: sessionId)) != .blinded25 else { return } guard (try? SessionId.Prefix(from: sessionId)) == .blinded15 else { - Storage.shared.write { db in - try SessionThread - .fetchOrCreate(db, id: sessionId, variant: .contact, shouldBeVisible: nil) + Storage.shared.write { [dependencies = viewModel.dependencies] db in + try SessionThread.upsert( + db, + id: sessionId, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) } let conversationVC: ConversationVC = ConversationVC( @@ -1277,7 +1306,7 @@ extension ConversationVC: return } - let targetThreadId: String? = Storage.shared.write { db in + let targetThreadId: String? = Storage.shared.write { [dependencies = viewModel.dependencies] db in let lookup: BlindedIdLookup = try BlindedIdLookup .fetchOrCreate( db, @@ -1287,14 +1316,14 @@ extension ConversationVC: isCheckingForOutbox: false ) - return try SessionThread - .fetchOrCreate( - db, - id: (lookup.sessionId ?? lookup.blindedId), - variant: .contact, - shouldBeVisible: nil - ) - .id + return try SessionThread.upsert( + db, + id: (lookup.sessionId ?? lookup.blindedId), + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ).id } guard let threadId: String = targetThreadId else { return } @@ -1511,7 +1540,11 @@ extension ConversationVC: if self?.viewModel.threadData.threadShouldBeVisible == false { _ = try SessionThread .filter(id: cellViewModel.threadId) - .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true)) + .updateAllAndConfig( + db, + SessionThread.Columns.shouldBeVisible.set(to: true), + using: dependencies + ) } let pendingReaction: Reaction? = { @@ -1745,7 +1778,7 @@ extension ConversationVC: roomToken: room, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .flatMap { successfullyAddedGroup in @@ -2109,6 +2142,7 @@ extension ConversationVC: } // Delete the message from the open group + self.hideInputAccessoryView() deleteRemotely( from: self, request: Storage.shared @@ -2133,13 +2167,14 @@ extension ConversationVC: userPublicKey : cellViewModel.threadId ) - let serverHash: String? = Storage.shared.read { db -> String? in - try Interaction - .select(.serverHash) - .filter(id: cellViewModel.id) - .asRequest(of: String.self) - .fetchOne(db) - } + let serverHashes: Set = Storage.shared + .read { db -> Set in + try Interaction.serverHashesForDeletion( + db, + interactionIds: [cellViewModel.id] + ) + } + .defaulting(to: []) let unsendRequest: UnsendRequest = UnsendRequest( timestamp: UInt64(cellViewModel.timestampMs), author: (cellViewModel.variant == .standardOutgoing ? @@ -2153,7 +2188,7 @@ extension ConversationVC: ) // For incoming interactions or interactions with no serverHash just delete them locally - guard cellViewModel.variant == .standardOutgoing, let serverHash: String = serverHash else { + guard cellViewModel.variant == .standardOutgoing, !serverHashes.isEmpty else { Storage.shared.writeAsync { db in _ = try Interaction .filter(id: cellViewModel.id) @@ -2161,7 +2196,7 @@ extension ConversationVC: // No need to send the unsendRequest if there is no serverHash (ie. the message // was outgoing but never got to the server) - guard serverHash != nil else { return } + guard !serverHashes.isEmpty else { return } MessageSender .send( @@ -2209,7 +2244,6 @@ extension ConversationVC: actionSheet.addAction(UIAlertAction( title: { switch (cellViewModel.threadVariant, cellViewModel.threadId) { - case (.legacyGroup, _), (.group, _): return "clearMessagesForEveryone".localized() case (_, userPublicKey): return "deleteMessageDevicesAll".localized() default: return "deleteMessageEveryone".localized() } @@ -2219,6 +2253,10 @@ extension ConversationVC: ) { [weak self] _ in let completeServerDeletion = { Storage.shared.writeAsync { db in + _ = try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + try MessageSender .send( db, @@ -2228,7 +2266,15 @@ extension ConversationVC: threadVariant: cellViewModel.threadVariant, using: dependencies ) + + /// We should also remove the `SnodeReceivedMessageInfo` entries for the hashes (otherwise we + /// might try to poll for a hash which no longer exists, resulting in fetching the last 14 days of messages) + try SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: Array(serverHashes) + ) } + self?.showInputAccessoryView() } // We can only delete messages on the server for `contact` and `group` conversations @@ -2241,7 +2287,7 @@ extension ConversationVC: request: SnodeAPI .deleteMessages( swarmPublicKey: targetPublicKey, - serverHashes: [serverHash] + serverHashes: Array(serverHashes) ) .map { _ in () } .eraseToAnyPublisher() @@ -2715,7 +2761,8 @@ extension ConversationVC { db, Contact.Columns.isApproved.set(to: true), Contact.Columns.didApproveMe - .set(to: contact.didApproveMe || !isNewThread) + .set(to: contact.didApproveMe || !isNewThread), + using: dependencies ) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) @@ -2745,7 +2792,8 @@ extension ConversationVC { tableView: self.tableView, threadViewModel: self.viewModel.threadData, viewController: self, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: viewModel.dependencies ) guard let action: UIContextualAction = actions?.first else { return } @@ -2769,7 +2817,8 @@ extension ConversationVC { tableView: self.tableView, threadViewModel: self.viewModel.threadData, viewController: self, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: viewModel.dependencies ) guard let action: UIContextualAction = actions?.first else { return } diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 3d89d4c11f5..2e99200bde1 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -782,18 +782,18 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold markAsReadPublisher = markAsReadTrigger .throttle(for: .milliseconds(100), scheduler: DispatchQueue.global(qos: .userInitiated), latest: true) .handleEvents( - receiveOutput: { [weak self] target, timestampMs in + receiveOutput: { [weak self, dependencies] target, timestampMs in let threadData: SessionThreadViewModel? = self?._threadData.wrappedValue switch target { - case .thread: threadData?.markAsRead(target: target) + case .thread: threadData?.markAsRead(target: target, using: dependencies) case .threadAndInteractions(let interactionId): guard timestampMs == nil || (self?.lastInteractionTimestampMsMarkedAsRead ?? 0) < (timestampMs ?? 0) || (self?.lastInteractionIdMarkedAsRead ?? 0) < (interactionId ?? 0) else { - threadData?.markAsRead(target: .thread) + threadData?.markAsRead(target: .thread, using: dependencies) return } @@ -804,7 +804,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold } self?.lastInteractionIdMarkedAsRead = (interactionId ?? threadData?.interactionId) - threadData?.markAsRead(target: target) + threadData?.markAsRead(target: target, using: dependencies) } } ) @@ -872,10 +872,14 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold let threadId: String = self.threadId let displayName: String = self._threadData.wrappedValue.displayName - Storage.shared.writeAsync { db in + Storage.shared.writeAsync { [dependencies] db in try Contact .filter(id: threadId) - .updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false)) + .updateAllAndConfig( + db, + Contact.Columns.isBlocked.set(to: false), + using: dependencies + ) } } diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift index 74cc4b82b92..26abc763278 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift @@ -375,23 +375,23 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga } // Contacts & legacy closed groups need to update the LibSession - dependencies.storage.writeAsync(using: dependencies) { [threadId, threadVariant] db in + dependencies.storage.writeAsync(using: dependencies) { [threadId, threadVariant, dependencies] db in switch threadVariant { case .contact: - try LibSession - .update( - db, - sessionId: threadId, - disappearingMessagesConfig: updatedConfig - ) + try LibSession.update( + db, + sessionId: threadId, + disappearingMessagesConfig: updatedConfig, + using: dependencies + ) case .legacyGroup: - try LibSession - .update( - db, - groupPublicKey: threadId, - disappearingConfig: updatedConfig - ) + try LibSession.update( + db, + groupPublicKey: threadId, + disappearingConfig: updatedConfig, + using: dependencies + ) default: break } diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 61ffd19f8a9..49ce1270281 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -157,7 +157,8 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi .updateAllAndConfig( db, Profile.Columns.nickname - .set(to: (updatedNickname.isEmpty ? nil : editedDisplayName)) + .set(to: (updatedNickname.isEmpty ? nil : editedDisplayName)), + using: dependencies ) } } @@ -537,7 +538,8 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi db, type: .leaveGroupAsync, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -774,8 +776,14 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi dependencies.storage.writeAsync { [dependencies] db in let currentUserSessionId: String = getUserHexEncodedPublicKey(db, using: dependencies) try selectedUsers.forEach { userId in - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: userId, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: userId, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) try LinkPreview( url: communityUrl, @@ -831,12 +839,13 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi ) { guard oldBlockedState != isBlocked else { return } - dependencies.storage.writeAsync { db in + dependencies.storage.writeAsync { [dependencies] db in try Contact .filter(id: threadId) .updateAllAndConfig( db, - Contact.Columns.isBlocked.set(to: isBlocked) + Contact.Columns.isBlocked.set(to: isBlocked), + using: dependencies ) } } diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index f6581a8c830..767917c6acb 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -393,12 +393,14 @@ extension GlobalSearchViewController { // If it's a one-to-one thread then make sure the thread exists before pushing to it (in case the // contact has been hidden) if threadVariant == .contact { - Storage.shared.write { db in - try SessionThread.fetchOrCreate( + Storage.shared.write { [dependencies] db in + try SessionThread.upsert( db, id: threadId, variant: threadVariant, - shouldBeVisible: nil // Don't change current state + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies ) } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index f9e5e732e5c..7f590ec6107 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -353,7 +353,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS ) // 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 { + if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, !Singleton.appContext.isNotInForeground { appDelegate.startPollersIfNeeded() } @@ -751,7 +751,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) @@ -773,7 +774,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) @@ -802,7 +804,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS }() let destructiveAction: UIContextualAction.SwipeAction = { switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember) { - case (.contact, true, _): return .clear + case (.contact, true, _): return .hide case (.legacyGroup, _, true), (.group, _, true), (.community, _, _): return .leave default: return .delete } @@ -820,7 +822,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) @@ -897,7 +900,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS @objc func createNewConversation() { let viewController = SessionHostingViewController( - rootView: StartConversationScreen(), + rootView: StartConversationScreen(using: viewModel.dependencies), customizedNavigationBackground: .backgroundSecondary ) viewController.setNavBarTitle("conversationsStart".localized()) @@ -912,7 +915,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS } func createNewDMFromDeepLink(sessionId: String) { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen(accountId: sessionId)) + let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen(accountId: sessionId, using: viewModel.dependencies)) viewController.setNavBarTitle( "messageNew" .putNumber(1) diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index 407bf0ebf46..e2e65aa99d6 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -204,11 +204,12 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O // Remove the one-to-one requests try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .deleteContactConversationAndMarkHidden, threadIds: threadInfo .filter { _, variant in variant == .contact } .map { id, _ in id }, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) // Remove the group requests @@ -218,7 +219,8 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O threadIds: threadInfo .filter { _, variant in variant == .legacyGroup || variant == .group } .map { id, _ in id }, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -257,7 +259,8 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O tableView: tableView, threadViewModel: threadViewModel, viewController: viewController, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: dependencies ) ) diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index d4d5db3639d..124a8ae01ea 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -9,12 +9,14 @@ import SessionSnodeKit struct NewMessageScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies @State var tabIndex = 0 @State private var accountIdOrONS: String @State private var errorString: String? = nil - init(accountId: String = "") { + init(accountId: String = "", using dependencies: Dependencies) { + self.dependencies = dependencies self.accountIdOrONS = accountId } @@ -122,7 +124,8 @@ struct NewMessageScreen: View { variant: .contact, action: .compose, dismissing: self.host.controller, - animated: false + animated: false, + using: dependencies ) } } @@ -202,5 +205,5 @@ struct EnterAccountIdScreen: View { } #Preview { - NewMessageScreen() + NewMessageScreen(using: Dependencies()) } diff --git a/Session/Home/New Conversation/StartConversationScreen.swift b/Session/Home/New Conversation/StartConversationScreen.swift index f3ea227cb90..866a4a1436c 100644 --- a/Session/Home/New Conversation/StartConversationScreen.swift +++ b/Session/Home/New Conversation/StartConversationScreen.swift @@ -7,6 +7,11 @@ import SessionUtilitiesKit struct StartConversationScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } var body: some View { ZStack(alignment: .topLeading) { @@ -26,7 +31,9 @@ struct StartConversationScreen: View { image: "Message", title: title ) { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen()) + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: NewMessageScreen(using: dependencies) + ) viewController.setNavBarTitle(title) viewController.setUpDismissingButton(on: .right) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) @@ -46,7 +53,7 @@ struct StartConversationScreen: View { image: "Group", title: "groupCreate".localized() ) { - let viewController = NewClosedGroupVC() + let viewController = NewClosedGroupVC(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } .accessibility( @@ -64,7 +71,7 @@ struct StartConversationScreen: View { image: "Globe", // stringlint:ignore title: "communityJoin".localized() ) { - let viewController = JoinOpenGroupVC() + let viewController = JoinOpenGroupVC(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } .accessibility( @@ -155,5 +162,5 @@ fileprivate struct NewConversationCell: View { } #Preview { - StartConversationScreen() + StartConversationScreen(using: Dependencies()) } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 86762ef3e42..1f728c06efc 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -30,6 +30,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Lifecycle func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Just in case we are running automated tests we should process environment variables + // before we do anything else + DeveloperSettingsViewModel.processUnitTestEnvVariablesIfNeeded() + Log.info("[AppDelegate] didFinishLaunchingWithOptions called.") startTime = CACurrentMediaTime() @@ -38,7 +42,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD verifyDBKeysAvailableBeforeBackgroundLaunch() _ = AppVersion.shared - AppEnvironment.shared.pushRegistrationManager.createVoipRegistryIfNecessary() + Singleton.setPushRegistrationManager(PushRegistrationManager(using: dependencies)) + Singleton.pushRegistrationManager.createVoipRegistryIfNecessary() // Prevent the device from sleeping during database view async registration // (e.g. long database upgrades). @@ -50,10 +55,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.loadingViewController = LoadingViewController() AppSetup.setupEnvironment( - appSpecificBlock: { + appSpecificBlock: { [dependencies] in Log.setup(with: Logger(primaryPrefix: "Session", level: .info)) Log.info("[AppDelegate] Setting up environment.") + /// Create a proper `SessionCallManager` for the main app (defaults to a no-op version) + Singleton.setCallManager(SessionCallManager(using: dependencies)) + // Setup LibSession LibSession.addLogger() LibSession.createNetworkIfNeeded() @@ -97,7 +105,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD using: dependencies ) - if SessionEnvironment.shared?.callManager.wrappedValue?.currentCall == nil { + if Singleton.callManager.currentCall == nil { UserDefaults.sharedLokiProject?[.isCallOngoing] = false UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil } @@ -687,7 +695,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Notifications func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - PushRegistrationManager.shared.didReceiveVanillaPushToken(deviceToken) + Singleton.pushRegistrationManager.didReceiveVanillaPushToken(deviceToken) Log.info("Registering for push notifications.") } @@ -696,9 +704,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD #if DEBUG Log.warn("We're in debug mode. Faking success for remote registration with a fake push identifier.") - PushRegistrationManager.shared.didReceiveVanillaPushToken(Data(count: 32)) + Singleton.pushRegistrationManager.didReceiveVanillaPushToken(Data(count: 32)) #else - PushRegistrationManager.shared.didFailToReceiveVanillaPushToken(error: error) + Singleton.pushRegistrationManager.didFailToReceiveVanillaPushToken(error: error) #endif } @@ -858,20 +866,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Call handling func hasIncomingCallWaiting() -> Bool { - guard let call = AppEnvironment.shared.callManager.currentCall else { return false } + guard let call = Singleton.callManager.currentCall else { return false } return !call.hasStartedConnecting } func hasCallOngoing() -> Bool { - guard let call = AppEnvironment.shared.callManager.currentCall else { return false } + guard let call = Singleton.callManager.currentCall else { return false } return !call.hasEnded } func handleAppActivatedWithOngoingCallIfNeeded() { guard - let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall), + let call: SessionCall = (Singleton.callManager.currentCall as? SessionCall), MiniCallView.current == nil, Singleton.hasAppContext else { return } diff --git a/Session/Meta/AppEnvironment.swift b/Session/Meta/AppEnvironment.swift index 6c9d0b27242..4ea25f00619 100644 --- a/Session/Meta/AppEnvironment.swift +++ b/Session/Meta/AppEnvironment.swift @@ -23,9 +23,7 @@ public class AppEnvironment { } } - public var callManager: SessionCallManager public var notificationPresenter: NotificationPresenter - public var pushRegistrationManager: PushRegistrationManager // Stored properties cannot be marked as `@available`, only classes and functions. // Instead, store a private `Any` and wrap it with a public `@available` getter @@ -36,9 +34,7 @@ public class AppEnvironment { } private init() { - self.callManager = SessionCallManager() self.notificationPresenter = NotificationPresenter() - self.pushRegistrationManager = PushRegistrationManager() self._userNotificationActionHandler = UserNotificationActionHandler() SwiftSingletons.register(self) @@ -46,9 +42,6 @@ public class AppEnvironment { public func setup() { // Hang certain singletons on Environment too. - SessionEnvironment.shared?.callManager.mutate { - $0 = callManager - } SessionEnvironment.shared?.notificationsManager.mutate { $0 = notificationPresenter } diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index fc02eb667c4..d84ce73639e 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -37,7 +37,8 @@ public struct SessionApp { variant: SessionThread.Variant, action: ConversationViewModel.Action = .none, dismissing presentingViewController: UIViewController?, - animated: Bool + animated: Bool, + using dependencies: Dependencies ) { let threadInfo: (threadExists: Bool, isMessageRequest: Bool)? = Storage.shared.read { db in let isMessageRequest: Bool = { @@ -84,7 +85,14 @@ public struct SessionApp { guard threadInfo?.threadExists == true else { DispatchQueue.global(qos: .userInitiated).async { Storage.shared.write { db in - try SessionThread.fetchOrCreate(db, id: threadId, variant: variant, shouldBeVisible: nil) + try SessionThread.upsert( + db, + id: threadId, + variant: variant, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) } // Send back to main thread for UI transitions diff --git a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist index 1e94962d94c..1dcc5edf1f3 100644 --- a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist +++ b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist @@ -981,6 +981,19 @@ SOFTWARE. Title Quick + + License + Copyright (C) 2015-2024 Gwendal Roué + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Title + session-grdb-swift + License The MIT License (MIT) @@ -1009,6 +1022,27 @@ SOFTWARE. Title session-ios-yyimage + + License + ISC License + +Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + Title + session-lucide + License diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index f08574087c9..ec8c5a80a45 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -4366,7 +4366,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Hey, I've been using {app_name} to chat with complete privacy and security. Come join me! My Account ID is

{account_id}

Download it at {session_download_url}" + "value" : "مرحبًا، لقد كنت أستخدم {app_name} للدردشة مع خصوصية وأمان كاملين. انضم إليّ! معرف حسابي هو

{account_id}

قم بتحميله من {session_download_url}" } }, "az" : { @@ -14982,7 +14982,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تمت إزالة {name} و{count} آخرين من منصبهم كمسؤولين." + "value" : "تمت إزالة {name} و{count} آخرين من منصبهم كمشرفين." } }, "az" : { @@ -15449,7 +15449,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تمت إزالة {name} و{other_name} من منصبي المسؤول." + "value" : "تمت إزالة {name} و{other_name} من منصبي المشرف." } }, "az" : { @@ -16395,7 +16395,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فشل في إزالة {name} و{count} آخرين كمسؤول." + "value" : "فشل في إزالة {name} و{count} آخرين كمشرف." } }, "az" : { @@ -16862,7 +16862,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فشل في إزالة {name} و{other_name} كمسؤول." + "value" : "فشل في إزالة {name} و{other_name} كمشرف." } }, "az" : { @@ -25615,7 +25615,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "كبِر" + "value" : "تكبير" } }, "az" : { @@ -28022,7 +28022,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ألبوم بدون إسم" + "value" : "البوم بدون اسم" } }, "az" : { @@ -32860,7 +32860,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نَزِل المرفق" + "value" : "تنزيل المرفق" } }, "az" : { @@ -46781,7 +46781,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الدقة أو الأبعاد:" + "value" : "دقة الشاشة:" } }, "az" : { @@ -51589,7 +51589,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "غير قادر على تشغيل ملف الصوت." + "value" : "تعذّر تشغيل الملف الصوتي" } }, "az" : { @@ -56894,7 +56894,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تم رفع الحظر عن المستخدم" + "value" : "تم رفع المنع عن المستخدم" } }, "az" : { @@ -57858,7 +57858,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تم حظر المستخدم" + "value" : "تم منع المستخدم" } }, "az" : { @@ -58822,7 +58822,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فك حظر هذه جهة الإتصال لإرسال رسالة" + "value" : "إلغاء حظر جهة الإتصال لإرسال رسالة" } }, "az" : { @@ -71063,11 +71063,35 @@ "callsPermissionsRequiredDescription1" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يمكنك تمكين إذن \"المكالمات الصوتية والفيديو\" في إعدادات الأذونات." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oprávnění pro \"Hlasové a video hovory\" můžete povolit v nastavení Oprávnění." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You can enable the \"Voice and Video Calls\" permission in Permissions Settings." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "U kunt de machtiging voor \"Spraak- en video-oproepen\" inschakelen in de privacy-instellingen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви можете увімкнути «Голосові та відеодзвінки» в налаштуваннях конфіденційності." + } } } }, @@ -74856,7 +74880,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Дозволяє здійснювати голосові та відео виклики з іншими користувачами." + "value" : "Дозволяє голосові та відеодзвінки з іншими користувачами." } }, "ur-IN" : { @@ -75814,7 +75838,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ви пропустили дзвінок від {name}, бо не увімкнули Голосові та Відеодзвінки у Налаштуваннях Приватності." + "value" : "Ви пропустили дзвінок від {name}, бо не увімкнули Голосові та відеодзвінки у налаштуваннях конфіденційності." } }, "ur-IN" : { @@ -115436,7 +115460,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Обрізка повідомлень" + "value" : "Автовидалення повідомлень" } }, "ur-IN" : { @@ -115915,7 +115939,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Обрізати спільноти" + "value" : "Автовидалення повідомлень спільнот" } }, "ur-IN" : { @@ -117890,7 +117914,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النقر على مفتاح الدخول سوف يرسل الرسالة بدلا من بدء سطر جديد." + "value" : "النقر على Enter سوف يرسل الرسالة بدلاَ من بدء سطر جديد." } }, "az" : { @@ -122704,49 +122728,49 @@ "af" : { "stringUnit" : { "state" : "translated", - "value" : "Ons het opgemerk {app_name} neem lank om te begin.

Jy kan aanhou wag, jou toestel logs uitvoer om te deel vir foutsporing, of probeer om Session te herbegin." + "value" : "Ons het opgemerk {app_name} neem lank om te begin.

Jy kan aanhou wag, jou toestel logs uitvoer om te deel vir foutsporing, of probeer om {app_name} te herbegin." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لقد لاحظنا أن {app_name} يستغرق وقتًا طويلاً لبدء.

يمكنك مواصلة الانتظار، تصدير سجلات الجهاز للمشاركة في استكشاف الأخطاء وإصلاحها، أو محاولة إعادة تشغيل Session." + "value" : "لقد لاحظنا أن {app_name} يستغرق وقتًا طويلاً لبدء.

يمكنك مواصلة الانتظار، تصدير سجلات الجهاز للمشاركة في استكشاف الأخطاء وإصلاحها، أو محاولة إعادة تشغيل {app_name}." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} tətbiqinin başladılmasının çox vaxt apardığına fikir verdik.

Gözləməyə davam edə, problemin aradan qaldırılması üçün cihazınızın jurnallarını xaricə köçürə və ya Session-u yenidən başlatmağa çalışa bilərsiniz." + "value" : "{app_name} tətbiqinin başladılmasının çox vaxt apardığına fikir verdik.

Gözləməyə davam edə, problemin aradan qaldırılması üçün cihazınızın jurnallarını xaricə köçürə və ya {app_name}-u yenidən başlatmağa çalışa bilərsiniz." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ما پٹِن گی اتہ {app_name} سرا کتن وقت گشتنت۔

باقی انتظار بکنت، سپاڈی بروک کردیا گزارش بکنت بیت ائی٬ گپچہ رفتارشت بار بکودنت بیت اتہ بھال دکنت۔" + "value" : "ما دیستگ کہ {app_name} ءِ بندات کنگ ءَ بازیں وھدے لگ اِیت۔

شما دیم ءَ اوشتات کن اِت، وتی ڈیوائس ءِ لاگاں پہ جیڑہ ءِ گیش ءُ گیوار ءَ شیئر کنگ ءِ ھاترا برآمد کن اِت یا {app_name} ءَ پدا بندات کنگ ءِ جُھد ءَ کن اِت۔" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Мы заўважылі, што {app_name} патрабуе шмат часу для запуску.

Вы можаце працягваць чакаць, экспартаваць журналы вашай прылады для спагнання праблем, альбо паспрабаваць перазапусціць Session." + "value" : "Мы заўважылі, што {app_name} патрабуе шмат часу для запуску.

Вы можаце працягваць чакаць, экспартаваць журналы вашай прылады для спагнання праблем, альбо паспрабаваць перазапусціць {app_name}." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Забелязахме, че стартирането на {app_name} отнема много време.

Можете да продължите да чакате, да експортирате дневници на устройството си, за да ги споделите за отстраняване на неизправности, или да опитате да рестартирате Session." + "value" : "Забелязахме, че стартирането на {app_name} отнема много време.

Можете да продължите да чакате, да експортирате дневници на устройството си, за да ги споделите за отстраняване на неизправности, или да опитате да рестартирате {app_name}." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session." + "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Hem notat que {app_name} està trigant molt a començar.

Podeu continuar esperant, exportar els registres del dispositiu per compartir-los per solucionar problemes, o intentar reiniciar Session." + "value" : "Hem notat que {app_name} està trigant molt a començar.

Podeu continuar esperant, exportar els registres del dispositiu per compartir-los per solucionar problemes, o intentar reiniciar {app_name}." } }, "cs" : { @@ -122764,7 +122788,7 @@ "da" : { "stringUnit" : { "state" : "translated", - "value" : "Vi har bemærket, at {app_name} tager lang tid at starte.

Du kan fortsætte med at vente, eksportere dine enhedslogfiler for at dele dem til fejlfinding eller prøve at genstarte Session." + "value" : "Vi har bemærket, at {app_name} tager lang tid at starte.

Du kan fortsætte med at vente, eksportere dine enhedslogfiler for at dele dem til fejlfinding eller prøve at genstarte {app_name}." } }, "de" : { @@ -122776,7 +122800,7 @@ "el" : { "stringUnit" : { "state" : "translated", - "value" : "Παρατηρήσαμε ότι το {app_name} χρειάζεται πολύ χρόνο για να ξεκινήσει.

Μπορείτε να συνεχίσετε να περιμένετε, να εξάγετε τα αρχεία καταγραφής της συσκευής σας για να τα μοιραστείτε για την αντιμετώπιση προβλημάτων ή να επανεκκινήσετε το Session." + "value" : "Παρατηρήσαμε ότι το {app_name} χρειάζεται πολύ χρόνο για να ξεκινήσει.

Μπορείτε να συνεχίσετε να περιμένετε, να εξάγετε τα αρχεία καταγραφής της συσκευής σας για να τα μοιραστείτε για την αντιμετώπιση προβλημάτων ή να επανεκκινήσετε το {app_name}." } }, "en" : { @@ -122806,13 +122830,13 @@ "et" : { "stringUnit" : { "state" : "translated", - "value" : "Oleme märganud, et {app_name} käivitamine võtab kaua aega.

Võite jätkata ootamist, eksportida oma seadme logisid tõrkeotsingu eesmärgil jagamiseks või proovida Session'i taaskäivitamist." + "value" : "Oleme märganud, et {app_name} käivitamine võtab kaua aega.

Võite jätkata ootamist, eksportida oma seadme logisid tõrkeotsingu eesmärgil jagamiseks või proovida {app_name}'i taaskäivitamist." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} martxan jartzeko denbora gehiegi hartzen ari dela nabaritu dugu.

Jarrai itzazu itxaroten, esportatu zure gailu-erregistroak konpontzeko partekatzeko edo saiatu Session berrabiarazten." + "value" : "{app_name} martxan jartzeko denbora gehiegi hartzen ari dela nabaritu dugu.

Jarrai itzazu itxaroten, esportatu zure gailu-erregistroak konpontzeko partekatzeko edo saiatu {app_name} berrabiarazten." } }, "fa" : { @@ -122824,31 +122848,31 @@ "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Huomasimme, että {app_name} käynnistyy hitaasti.

Voit jatkaa odottamista, viedä laitteesi lokit jaettavaksi vianmäärityksessä tai yrittää käynnistää Sessionin uudelleen." + "value" : "Huomasimme, että {app_name} käynnistyy hitaasti.

Voit jatkaa odottamista, viedä laitteesi lokit jaettavaksi vianmäärityksessä tai yrittää käynnistää {app_name} uudelleen." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Napansin naming matagal bago mag-start ang {app_name}.

Puwede kang maghintay nalang, i-export ang iyong device logs para i-share para sa troubleshooting, o subukan i-restart ang Session." + "value" : "Napansin naming matagal bago mag-start ang {app_name}.

Puwede kang maghintay nalang, i-export ang iyong device logs para i-share para sa troubleshooting, o subukan i-restart ang {app_name}." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nous avons remarqué que {app_name} met beaucoup de temps à démarrer.

Vous pouvez continuer à attendre, exporter les journaux de votre appareil pour les partager pour le dépannage ou essayer de redémarrer Session." + "value" : "Nous avons remarqué que {app_name} met beaucoup de temps à démarrer.

Vous pouvez continuer à attendre, exporter les journaux de votre appareil pour les partager pour le dépannage ou essayer de redémarrer {app_name}." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Notamos que {app_name} está a tardar moito en iniciar.

Podes esperar, exportar os teus rexistros do dispositivo para compartir e solucionar problemas, ou tentar reiniciar Session." + "value" : "Notamos que {app_name} está a tardar moito en iniciar.

Podes esperar, exportar os teus rexistros do dispositivo para compartir e solucionar problemas, ou tentar reiniciar {app_name}." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Mun lura cewa {app_name} yana ɗaukar dogon lokaci don farawa.

Za ku iya ci gaba da jira, fitar da log ɗin na'urarku don rabawa don magance matsaloli, ko sake farawa Session." + "value" : "Mun lura cewa {app_name} yana ɗaukar dogon lokaci don farawa.

Za ku iya ci gaba da jira, fitar da log ɗin na'urarku don rabawa don magance matsaloli, ko sake farawa {app_name}." } }, "he" : { @@ -122866,7 +122890,7 @@ "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Primijetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti čekati, izvesti zapise uređaja za dijeljenje radi rješavanja problema ili pokušati ponovo pokrenuti Session." + "value" : "Primijetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti čekati, izvesti zapise uređaja za dijeljenje radi rješavanja problema ili pokušati ponovo pokrenuti {app_name}." } }, "hu" : { @@ -122902,7 +122926,7 @@ "ka" : { "stringUnit" : { "state" : "translated", - "value" : "გავიგეთ {app_name}-ის გაშვება დიდ დროს იკავებს.

თქვენ შეგიძლიათ დაელოდოთ, ექსპორტირდეთ თქვენი მოწყობილობის ჟურნალები რათა გაუზიაროთ პრობლემების დიაგნოსტირებისთვის, ან სცადოთ Session-ის გადატვირთვა." + "value" : "გავიგეთ {app_name}-ის გაშვება დიდ დროს იკავებს.

თქვენ შეგიძლიათ დაელოდოთ, ექსპორტირდეთ თქვენი მოწყობილობის ჟურნალები რათა გაუზიაროთ პრობლემების დიაგნოსტირებისთვის, ან სცადოთ {app_name}-ის გადატვირთვა." } }, "km" : { @@ -122974,7 +122998,7 @@ "my" : { "stringUnit" : { "state" : "translated", - "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session." + "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." } }, "nb" : { @@ -123010,7 +123034,7 @@ "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Timazindikira {app_name} kutenga nthawi kuti ayambe.

Inu mungapitirize kudikira, kutulutsira chipangizo malipoti kuti azipeza mavuto, kapena yesani kuyambiranso Session." + "value" : "Timazindikira {app_name} kutenga nthawi kuti ayambe.

Inu mungapitirize kudikira, kutulutsira chipangizo malipoti kuti azipeza mavuto, kapena yesani kuyambiranso {app_name}." } }, "pa-IN" : { @@ -125117,7 +125141,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أحذف" + "value" : "حذف" } }, "az" : { @@ -135222,6 +135246,46 @@ } } } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити це повідомлення?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення?" + } + } + } + } + } + } } } }, @@ -138451,6 +138515,58 @@ "deleteMessageDescriptionDevice" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسالة من هذا الجهاز فقط؟" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + } + } + } + } + } + }, "cs" : { "stringUnit" : { "state" : "translated", @@ -138491,6 +138607,34 @@ } } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, du möchtest diese Nachrichten nur von diesem Gerät löschen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du diese Nachrichten wirklich nur von diesem Gerät löschen?" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -138586,6 +138730,46 @@ } } } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення лише з цього пристрою?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення лише з цього пристрою?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити це повідомлення лише з цього пристрою?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення лише з цього пристрою?" + } + } + } + } + } + } } } }, @@ -142835,6 +143019,58 @@ "deleteMessageNoteToSelfWarning" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف هذه الرسالة من جميع أجهزتك" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + } + } + } + } + } + }, "cs" : { "stringUnit" : { "state" : "translated", @@ -154538,7 +154774,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فقط المسؤولين يمكنهم تغيير هذا الإعداد." + "value" : "يمكن لمشرفين المجموعة فقط تغيير هذا الإعداد." } }, "az" : { @@ -157430,7 +157666,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "{name} قام/ت بإيقاف تشغيل الرسائل المختفية إيقاف." + "value" : "{name} قام بإيقاف تشغيل الرسائل المختفية إيقاف." } }, "az" : { @@ -168027,7 +168263,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إيموجي & رموز" + "value" : "إيموجي و رموز" } }, "az" : { @@ -169961,7 +170197,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مأكولات & و مشروبات" + "value" : "مأكولات و مشروبات" } }, "az" : { @@ -171398,7 +171634,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ابتسامات & وأشخاص" + "value" : "ابتسامات وأشخاص" } }, "az" : { @@ -172356,7 +172592,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "السفر & و أماكن" + "value" : "السفر و أماكن" } }, "az" : { @@ -178166,7 +178402,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تفاعل مع رسالتك {emoji}" + "value" : "تفاعل مع رسالتك بـ {emoji}" } }, "az" : { @@ -183938,7 +184174,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "صورة GIF" + "value" : "GIF" } }, "az" : { @@ -187782,14 +188018,43 @@ } } }, + "groupDeleteDescriptionMember" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to delete {group_name}?" + } + } + } + }, "groupDeletedMemberDescription" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupina {group_name} byla smazána správcem skupiny. Nebudete moci posílat další zprávy." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{group_name} has been deleted by a group admin. You will not be able to send any more messages." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} is verwijderd door een groepsbeheerder. U kunt geen berichten meer versturen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} видалено адміністратором групи. Ви більше не зможете надсилати повідомлення." + } } } }, @@ -193789,6 +194054,46 @@ } } } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошень" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошень" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошення" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошень" + } + } + } + } + } + } } } }, @@ -194283,7 +194588,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الدعوة إلى المجموعة ناجحة" + "value" : "تمت دعوة المجموعة بنجاح" } }, "az" : { @@ -203811,460 +204116,10 @@ "groupMemberNewYouHistoryTwo" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jy en {name} is genooi om by die groep aan te sluit. Kletsgeskiedenis is gedeel." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "أنت و{name} انضموا للمجموعة. تمت مشاركة سجل الدردشة." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siz{name} qrupa qoşulmaq üçün dəvət edildiniz. Söhbət tarixçəsi paylaşıldı." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "Šumār a {name} šumār zant group ke. Chat history was shared." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы і {name} былі запрошаны далучыцца да групы. Гісторыя чатаў была абагулена." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вие и {name} бяхте поканени да се присъедините към групата. История на чатовете беше споделена." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনি এবং {name} গ্রুপে যোগ দেওয়ার জন্য আমন্ত্রিত হয়েছে। চ্যাট ইতিহাস শেয়ার করা হয়েছে।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu i {name} heu estat convidats a unir-vos al grup. S'ha compartit l'historial de la conversa." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vy a {name} byli pozváni do skupiny. Historie konverzace byla sdílena." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chi a {name} ymunodd â'r grŵp. Hanes sgwrs wedi cael ei rhannu." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} blev inviteret til at deltage i gruppen. Chat historik blev delt." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du und {name} wurden eingeladen, der Gruppe beizutreten. Der Chatverlauf wurde freigegeben." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Εσείς και {name} προσκληθήκατε να συμμετάσχετε στην ομάδα. Το ιστορικό συνομιλίας κοινοποιήθηκε." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "You and {name} were invited to join the group. Chat history was shared." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi kaj {name} estis invititaj aniĝi al la grupo. Babilhistorio estis dividita." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : " y {name} fueron invitados a unirse al grupo. El historial de chat fue compartido." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : " y {name} fueron invitados a unirse al grupo. El historial de chat fue compartido." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sind ja {name} kutsuti grupiga liituma. Vestluse ajalugu jagati nendega." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zuk eta {name} taldera batzeko gonbidatu zaituzte. Txat historia partekatu da." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "شما و {name} دعوت شدید تا به گروه بپیوندید. تاریخچه ی چت به اشتراک گذاشته شد." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sinä ja {name} kutsuttiin ryhmään. Keskusteluhistoria jaetaan." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikaw at {name} ay naimbitahan na sumali sa grupo. Ibinahagi ang kasaysayan ng chat." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vous et {name} avez été invité·e·s à rejoindre le groupe. L'historique de discussion a été partagé." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ku da {name} an gayyace ku shiga ƙungiyar. An raba tarihin hira." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "את/ה ו{name}‏ הוזמנתם להצטרף לקבוצה. היסטוריית הצ'אט שותפה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आप और {name} को समूह में शामिल होने के लिए आमंत्रित किया गया। चैट इतिहास साझा किया गया।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi i {name} pozvani ste da se pridružite grupi. Povijest razgovora je podijeljena." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Te és {name} meg lettetek hívva a csoportba. A beszélgetési előzményeket megosztottuk." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Դուք և {name}֊ը հրավիրվել են միանալու խմբին: Զրույցի պատմությունը կիսվել է:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anda dan {name} telah diundang untuk bergabung dengan grup. Riwayat obrolan dibagikan." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu e {name} avete ricevuto un invito a unirvi al gruppo. La cronologia della chat è condivisa." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "あなた{name} がグループに招待されました。チャット履歴が共有されました。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენ და {name} მიწვეული იყავით ჯგუფში. ჩეთის ისტორია გაზიარდა." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "អ្នក និង {name} ត្រូវបានអញ្ជើញឱ្យចូលក្រុមនេះ។បានចែករំលែកប្រវត្តិការជជែក។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನೀವು ಮತ್ತು {name} ಅವರಿಗೆ ಗುಂಪಿಗೆ ಸೇರಲು ಆಹ್ವಾನಿಸಲಾಗಿದೆ. ಚಾಟ್ ಇತಿಹಾಸವನ್ನು ಹಂಚಲಾಗಿದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "당신{name}이 그룹에 초대받았습니다. 대화 내역이 공개됩니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "تۆ و {name} بانگکران بۆ بەشداریکردن لە گروپەکە. مێژوو بگردەوەیی پەیامەکان سییبرەیە." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Te û {name} hatin dawetin ku tevlî komê bibin. Dîroka sohbetê hate parve kirin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ggwe ne {name} mwakuyitibwa okwegatta mu kibiina. Ebika by'obubaka by'akugabana." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūs ir {name} buvo pakviesti prisijungti prie grupės. Pokalbio istorija buvo pasidalinta." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вие и {name} беа поканети да се придружат на групата. Историјата на разговорот е споделена." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Та болон {name} бүлэгт нэгдэх урилга авсан байна. Чатын түүх хуваалцагдсан." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anda dan {name} dijemput untuk menyertai kumpulan. Sejarah sembang telah dikongsi." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်နှင့် {name} အဖွဲ့သို့ ဖိတ်ကြားခံရပြီ။ စကားဝိုင်းမှတ်တမ်းကိုမျှဝေခဲ့သည်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} ble invitert til gruppen. Chat-historikk ble delt." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} ble invitert til å bli med i gruppen. Chat-historikk ble delt." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईं{name}लाई समूहमा सामेल हुन आमन्त्रित गरियो। च्याट इतिहास सेयर गरियो।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "U en {name} zijn uitgenodigd om lid te worden van de groep. Geschiedenis van het gesprek is gedeeld." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} vart invitert til å bli med i gruppa. Chathistorikk vart delt." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inu ndi {name} anaitanidwa kuti alowe mu gulu. Mbiri ya macheza idagawidwa." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ty oraz użytkownik {name} zostaliście zaproszeni do grupy. Udostępniono historię czatu." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "تاسو او {name} ډله کې ګډون کولو ته بلل شوی. د خبرو تاریخ شریک شوی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Você e {name} foram convidados a participar do grupo. O histórico de conversas foi compartilhado." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Você e {name} foram convidados a juntar-se ao grupo. O histórico da conversa foi partilhado." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu și {name} ați fost invitați să vă alăturați grupului. Istoricul conversațiilor a fost partajat." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы и пользователь {name} приглашены вступить в группу. История чата была передана." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ti i {name} ste pozvani da se pridružite grupi. Istorija razgovora je deljena." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබ සහ {name} කණ්ඩායමට සම්බන්ධ වන්නට ආරාධනා කරන ලදී. සංවාද ඉතිහාසය බෙදා ගන්නා ලදී." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vy a {name} ste boli pozvaní, aby ste sa pripojili do skupiny. História chatu bola zdieľaná." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi in {name} sta bila povabljena, da se pridružita skupini. Zgodovina klepeta je bila deljena." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ju dhe {name} u ftuat të bashkoheni me grupin. Historia e bisedës u ndanë." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви и {name} су позвани да се придруже групи. Историја ћаскања је подељена." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi i {name} ste pozvani da se pridružite grupi. Istorija četa je podeljena." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du och {name} bjöds in att gå med i gruppen. Chatt historik delades." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wewe na {name} mmealikwa kujiunga na kundi. Historia ya gumzo ilishirikiwa." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "நீங்கள் மற்றும் {name} குழுவில் சேர்க்கப்பட்டீர்கள். உரையாடல் வரலாறு பகிரப்பட்டது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీరు మరియు {name} సమూహంలో చేరడానికి ఆహ్వానించబడ్డారు. చాట్ చరిత్ర పంచబడింది." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "คุณ และ {name} ถูกเชิญเข้าร่วมกลุ่ม ประวัติการแชทถูกแชร์" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sen ve {name} gruba katılmaya davet edildiniz. Sohbet geçmişi paylaşıldı." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви та {name} були запрошені приєднатися до групи. Було надано спільний доступ до історії чату." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آپ اور {name} گروپ میں شامل ہونے کی دعوت دی گئی۔ چیٹ تاریخ شیئر کی گئی۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siz va {name} guruhga qo'shildi. Suhbat tarixini ko'rish imkoniyati berilgan." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bạn{name} đã được mời tham gia nhóm. Lịch sử trò chuyện đã được chia sẻ." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mna kunye {name} babememelwe ukuba bajoyine iqela. Imbali yencoko yenziwe yabelwana ngayo." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name}被邀请加入了群组。 聊天记录已共享。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name} 加入了群組。聊天記錄已分享。" + "value" : "You and {other_name} were invited to join the group. Chat history was shared." } } } @@ -207646,7 +207501,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أسم المجموعة الآن '{group_name}." + "value" : "اسم المجموعة الآن '{group_name}." } }, "az" : { @@ -209567,6 +209422,17 @@ } } }, + "groupPendingRemoval" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pending removal" + } + } + } + }, "groupPromotedYou" : { "extractionState" : "manual", "localizations" : { @@ -223772,7 +223638,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اصدر السجلات الخاصة بك، ثم ارفع الملف عبر مكتب المساعدة الخاص بـ{app_name}." + "value" : "إصدار السجلات الخاصة بك، ثم رفع الملف عبر مكتب المساعدة الخاص بـ{app_name}." } }, "az" : { @@ -226652,7 +226518,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Toggle system menu bar visibility" + "value" : "تبديل رؤية شريط قائمة النظام" } }, "az" : { @@ -235544,7 +235410,7 @@ "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Previzualizări ale linkurilor" + "value" : "Podgląd linków" } }, "ps" : { @@ -238538,7 +238404,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Надсилати попередній перегляд посилань" + "value" : "Попередній перегляд надісланих посилань" } }, "ur-IN" : { @@ -248332,19 +248198,19 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "other" : { @@ -248356,13 +248222,13 @@ "two" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "zero" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } } } @@ -250367,7 +250233,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "دعوة المتصلين" + "value" : "دعوة جهات الاتصال" } }, "az" : { @@ -259366,7 +259232,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اعتبرها مقروءة" + "value" : "تحديد كـ \"مقروء\"" } }, "az" : { @@ -259845,7 +259711,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اجعله/ها غير مقروءة" + "value" : "تحديد كـ \"غير مقروء\"" } }, "az" : { @@ -266110,7 +265976,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إرسال رسالة إلى هذه المجموعة سوف يقبل تلقائيًا دعوة المجموعة." + "value" : "بإرسال رسالة إلى هذه المجموعة سوف يقبل تلقائيًا دعوة المجموعة." } }, "az" : { @@ -267547,7 +267413,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إرسال رسالة إلى هذا المستخدم سوف يقبل تلقائيًا طلب الرسالة الخاص به ويكشف عن معرف حسابك." + "value" : "بإرسال رسالة إلى هذا المستخدم سوف يقبل تلقائيًا طلب الرسالة الخاص به ويكشف عن معرف حسابك." } }, "az" : { @@ -270912,7 +270778,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد طلبات رسالة معلقة" + "value" : "لا توجد طلبات مراسلة معلقة" } }, "az" : { @@ -272834,7 +272700,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حدد الرسالة" + "value" : "تحديد رسالة" } }, "az" : { @@ -276175,7 +276041,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اضغط باستمرار لتسجيل رسالة صوتية" + "value" : "اضغط مع الاستمرار لتسجيل رسالة صوتية" } }, "az" : { @@ -278067,7 +277933,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "صغِّر" + "value" : "تصغير" } }, "az" : { @@ -283359,7 +283225,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إخفاء ملاحظة لنفسي" + "value" : "إخفاء \"ملاحظة لنفسي\"" } }, "az" : { @@ -285287,7 +285153,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "المعلومات معروضة في الإشعارات." + "value" : "المعلومات المعروضة في الإشعارات." } }, "az" : { @@ -288640,7 +288506,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اذهب إلى إعدادات تنبيهات الجهاز" + "value" : "اذهب إلى إعدادات إشعارات الجهاز" } }, "az" : { @@ -289119,7 +288985,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التنبيهات - الكل" + "value" : "الإشعارات - الكل" } }, "az" : { @@ -289586,7 +289452,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التنبيهات - الإشعارات فقط" + "value" : "الإشعارات- الإشارات فقط" } }, "az" : { @@ -290053,7 +289919,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التنبيهات - مكتومة" + "value" : "الإشعارات - مكتومة" } }, "az" : { @@ -298149,7 +298015,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Стратегія сповіщення" + "value" : "Принцип оповіщення" } }, "ur-IN" : { @@ -300603,7 +300469,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نعم" + "value" : "حسناً" } }, "az" : { @@ -317404,7 +317270,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرجى إدخال كلمة السر الحالية" + "value" : "الرجاء إدخال كلمة السر الحالية" } }, "az" : { @@ -317877,7 +317743,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرجى إدخال كلمة السر الجديدة" + "value" : "الرجاء إدخال كلمة السر الجديدة" } }, "az" : { @@ -334139,7 +334005,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ثَبِت المحادثة" + "value" : "تثبيت المحادثة" } }, "az" : { @@ -335097,7 +334963,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ألغِي تثبيت المحادثة" + "value" : "إلغاء تثبيت المحادثة" } }, "az" : { @@ -339893,7 +339759,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "رمز QR هذا لا يحتوي على معرف حساب" + "value" : "رمز QR هذا لا يحتوي على مُعرف حساب" } }, "az" : { @@ -340851,7 +340717,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "امسح رمز الاستجابة السريعة" + "value" : "امسح رمز الاستجابة السريعة QR" } }, "az" : { @@ -351868,7 +351734,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إخفاء كلمة المرور للاسترجاع" + "value" : "إخفاء كلمة مرور الاسترداد" } }, "az" : { @@ -364328,7 +364194,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الرجاء إدخال كملة بحث." + "value" : "الرجاء إدخال كلمة للبحث." } }, "az" : { @@ -364834,7 +364700,7 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "{found_count} من %lld إجابة" + "value" : "{found_count} من %lld مطابقة" } }, "other" : { @@ -364846,7 +364712,7 @@ "two" : { "stringUnit" : { "state" : "translated", - "value" : "{found_count} من %lld مطابقات" + "value" : "{found_count} من %lld مطابقتين" } }, "zero" : { @@ -366833,7 +366699,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لم يتم العثور على أية نتيجة لـ {query}" + "value" : "لم يتم العثور على نتائج لـ {query}" } }, "az" : { @@ -372599,7 +372465,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أُدعُ صديق" + "value" : "دعوة صديق" } }, "az" : { @@ -381209,7 +381075,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اِذهب اِلى صفحة الدعم" + "value" : "الذهاب لصفحة الدعم" } }, "az" : { @@ -381688,7 +381554,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "System Information: {information}" + "value" : "معلومات النظام: {information}" } }, "az" : { @@ -384994,7 +384860,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Бачити та надсилати індикатори набору тексту." + "value" : "Бачити та надсилати індикатори введення тексту." } }, "ur-IN" : { @@ -394178,7 +394044,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "غير قادر على تشغيل الفيديو." + "value" : "تعذر تشغيل الفيديو" } }, "az" : { diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 7d20c552836..1e60c809ec3 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -480,7 +480,7 @@ class NotificationActionHandler { // MARK: - - func markAsRead(userInfo: [AnyHashable: Any]) -> AnyPublisher { + func markAsRead(userInfo: [AnyHashable: Any], using dependencies: Dependencies) -> AnyPublisher { guard let threadId: String = userInfo[AppNotificationUserInfoKey.threadId] as? String else { return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil")) .eraseToAnyPublisher() @@ -491,7 +491,7 @@ class NotificationActionHandler { .eraseToAnyPublisher() } - return markAsRead(threadId: threadId) + return markAsRead(threadId: threadId, using: dependencies) } func reply( @@ -532,7 +532,8 @@ class NotificationActionHandler { db, threadId: threadId, threadVariant: thread.variant - ) + ), + using: dependencies ) return try MessageSender.preparedSendData( @@ -576,7 +577,8 @@ class NotificationActionHandler { for: threadId, variant: threadVariant, dismissing: nil, - animated: (UIApplication.shared.applicationState == .active) + animated: (UIApplication.shared.applicationState == .active), + using: dependencies ) return Just(()) @@ -589,7 +591,7 @@ class NotificationActionHandler { .eraseToAnyPublisher() } - private func markAsRead(threadId: String) -> AnyPublisher { + private func markAsRead(threadId: String, using dependencies: Dependencies) -> AnyPublisher { return Storage.shared .writePublisher { db in guard @@ -616,7 +618,8 @@ class NotificationActionHandler { db, threadId: threadId, threadVariant: threadVariant - ) + ), + using: dependencies ) } .eraseToAnyPublisher() diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 3987ed35c50..6919fc91ccd 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -8,17 +8,22 @@ import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -public enum PushRegistrationError: Error { - case assertionError(description: String) - case pushNotSupported(description: String) - case timeout - case publisherNoLongerExists +// MARK: - Singleton + +public extension Singleton { + // FIXME: This will be reworked to be part of dependencies in the Groups Rebuild branch + fileprivate static var _pushRegistrationManager: Atomic = Atomic(NoopPushRegistrationManager()) + static var pushRegistrationManager: PushRegistrationManagerType { _pushRegistrationManager.wrappedValue } + + static func setPushRegistrationManager(_ pushRegistrationManager: PushRegistrationManagerType) { + _pushRegistrationManager = Atomic(pushRegistrationManager) + } } -/** - * Singleton used to integrate with push notification services - registration and routing received remote notifications. - */ -@objc public class PushRegistrationManager: NSObject, PKPushRegistryDelegate { +// MARK: - PushRegistrationManager + +public class PushRegistrationManager: NSObject, PKPushRegistryDelegate, PushRegistrationManagerType { + private let dependencies: Dependencies // MARK: - Dependencies @@ -26,27 +31,20 @@ public enum PushRegistrationError: Error { return AppEnvironment.shared.notificationPresenter } - // MARK: - Singleton class - - @objc - public static var shared: PushRegistrationManager { - get { - return AppEnvironment.shared.pushRegistrationManager - } - } - - override init() { - super.init() - - SwiftSingletons.register(self) - } - private var vanillaTokenPublisher: AnyPublisher? private var vanillaTokenResolver: ((Result) -> ())? private var voipRegistry: PKPushRegistry? private var voipTokenPublisher: AnyPublisher? private var voipTokenResolver: ((Result) -> ())? + + // MARK: - Initialization + + public init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init() + } // MARK: - Public interface @@ -72,7 +70,7 @@ public enum PushRegistrationError: Error { // MARK: Vanilla push token // Vanilla push token is obtained from the system via AppDelegate - public func didReceiveVanillaPushToken(_ tokenData: Data, using dependencies: Dependencies = Dependencies()) { + public func didReceiveVanillaPushToken(_ tokenData: Data) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { Log.error("[PushRegistrationManager] Publisher completion in \(#function) unexpectedly nil") return @@ -83,8 +81,8 @@ public enum PushRegistrationError: Error { } } - // Vanilla push token is obtained from the system via AppDelegate - public func didFailToReceiveVanillaPushToken(error: Error, using dependencies: Dependencies = Dependencies()) { + // Vanilla push token is obtained from the system via AppDelegate + public func didFailToReceiveVanillaPushToken(error: Error) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { Log.error("[PushRegistrationManager] Publisher completion in \(#function) unexpectedly nil") return @@ -284,9 +282,10 @@ public enum PushRegistrationError: Error { guard let uuid: String = payload["uuid"] as? String, let caller: String = payload["caller"] as? String, - let timestampMs: Int64 = payload["timestamp"] as? Int64 + let timestampMs: UInt64 = payload["timestamp"] as? UInt64, + TimestampUtils.isWithinOneMinute(timestampMs: timestampMs) else { - SessionCallManager.reportFakeCall(info: "Missing payload data") // stringlint:ignore + SessionCallManager.reportFakeCall(info: "Missing payload data", using: dependencies) // stringlint:ignore return } @@ -297,55 +296,54 @@ public enum PushRegistrationError: Error { LibSession.resumeNetworkAccess() let maybeCall: SessionCall? = Storage.shared.write { db in - let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( - state: (caller == getUserHexEncodedPublicKey(db) ? - .outgoing : - .incoming - ) - ) - - let messageInfoString: String? = { - if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { - return String(data: messageInfoData, encoding: .utf8) - } else { - return "callsIncoming" - .put(key: "name", value: caller) - .localized() - } - }() - - let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: caller, variant: .contact, shouldBeVisible: nil) + var call: SessionCall? = nil - let interaction: Interaction = try Interaction( - messageUuid: uuid, - threadId: thread.id, - threadVariant: thread.variant, - authorId: caller, - variant: .infoCall, - body: messageInfoString, - timestampMs: timestampMs - ) - .withDisappearingMessagesConfiguration(db, threadVariant: thread.variant) - .inserted(db) - - call.callInteractionId = interaction.id + do { + call = SessionCall( + db, + for: caller, + uuid: uuid, + mode: .answer, + using: dependencies + ) + + let thread: SessionThread = try SessionThread.upsert( + db, + id: caller, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) + + let interaction: Interaction? = try Interaction + .filter(Interaction.Columns.threadId == thread.id) + .filter(Interaction.Columns.messageUuid == uuid) + .fetchOne(db) + + call?.callInteractionId = interaction?.id + } catch { + SNLog("[Calls] Failed to create call due to error: \(error)") + } return call } guard let call: SessionCall = maybeCall else { - SessionCallManager.reportFakeCall(info: "Could not retrieve call from database") // stringlint:ignore + SessionCallManager.reportFakeCall(info: "Could not retrieve call from database", using: dependencies) // stringlint:ignore return } + JobRunner.appDidBecomeActive() + // NOTE: Just start 1-1 poller so that it won't wait for polling group messages (UIApplication.shared.delegate as? AppDelegate)?.startPollersIfNeeded(shouldStartGroupPollers: false) call.reportIncomingCallIfNeeded { error in if let error = error { SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") + } else { + SNLog("[Calls] Succeeded to report incoming call to CallKit") } } } @@ -357,3 +355,12 @@ fileprivate extension Data { return map { String(format: "%02hhx", $0) }.joined() // stringlint:ignore } } + +// MARK: - PushRegistrationError + +public enum PushRegistrationError: Error { + case assertionError(description: String) + case pushNotSupported(description: String) + case timeout + case publisherNoLongerExists +} diff --git a/Session/Notifications/PushRegistrationManagerType.swift b/Session/Notifications/PushRegistrationManagerType.swift new file mode 100644 index 00000000000..8b2948e7b15 --- /dev/null +++ b/Session/Notifications/PushRegistrationManagerType.swift @@ -0,0 +1,29 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine + +// FIXME: Remove this in Groups Rebuild (redundant with the updated dependency management) +public protocol PushRegistrationManagerType { + func createVoipRegistryIfNecessary() + func didReceiveVanillaPushToken(_ tokenData: Data) + func didFailToReceiveVanillaPushToken(error: Error) + + func requestPushTokens() -> AnyPublisher<(pushToken: String, voipToken: String), Error> +} + +// MARK: - NoopPushRegistrationManager + +public class NoopPushRegistrationManager: PushRegistrationManagerType { + public func createVoipRegistryIfNecessary() {} + public func didReceiveVanillaPushToken(_ tokenData: Data) {} + public func didFailToReceiveVanillaPushToken(error: Error) {} + + public func requestPushTokens() -> AnyPublisher<(pushToken: String, voipToken: String), Error> { + return Fail( + error: PushRegistrationError.assertionError( + description: "Attempted to register with NoopPushRegistrationManager" // stringlint:ignore + ) + ).eraseToAnyPublisher() + } +} diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index fdcba678927..3c86c54d926 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -104,7 +104,7 @@ public enum SyncPushTokensJob: JobExecutor { /// **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 Log.info("[SyncPushTokensJob] Re-registering for remote notifications") - PushRegistrationManager.shared.requestPushTokens() + Singleton.pushRegistrationManager.requestPushTokens() .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<(String, String)?, Error> in Deferred { Future<(String, String)?, Error> { resolver in diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index b70c33acf10..4e0b38a7a9c 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -294,7 +294,7 @@ public class UserNotificationActionHandler: NSObject { switch action { case .markAsRead: - return actionHandler.markAsRead(userInfo: userInfo) + return actionHandler.markAsRead(userInfo: userInfo, using: dependencies) case .reply: guard let textInputResponse = response as? UNTextInputNotificationResponse else { diff --git a/Session/Onboarding/DisplayNameScreen.swift b/Session/Onboarding/DisplayNameScreen.swift index 1551f674339..efb7011909e 100644 --- a/Session/Onboarding/DisplayNameScreen.swift +++ b/Session/Onboarding/DisplayNameScreen.swift @@ -119,7 +119,7 @@ struct DisplayNameScreen: View { // If we are not in the registration flow then we are finished and should go straight // to the home screen guard self.flow == .register else { - self.flow.completeRegistration() + self.flow.completeRegistration(using: dependencies) let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index 9010591d822..9514eacc408 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -108,7 +108,7 @@ struct LoadingScreen: View { self.percentage = 1 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [dependencies] in - self.flow.completeRegistration() + self.flow.completeRegistration(using: dependencies) let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index f5a770bb3e2..89e8a80ca32 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -162,21 +162,29 @@ enum Onboarding { db, Contact.Columns.isTrusted.set(to: true), // Always trust the current user Contact.Columns.isApproved.set(to: true), - Contact.Columns.didApproveMe.set(to: true) + Contact.Columns.didApproveMe.set(to: true), + using: dependencies ) /// Create the 'Note to Self' thread (not visible by default) /// /// **Note:** We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false` /// otherwise it won't actually get synced correctly - try SessionThread - .fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false) + try SessionThread.upsert( + db, + id: x25519PublicKey, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + calledFromConfig: nil, + using: dependencies + ) try SessionThread .filter(id: x25519PublicKey) .updateAllAndConfig( db, - SessionThread.Columns.shouldBeVisible.set(to: false) + SessionThread.Columns.shouldBeVisible.set(to: false), + using: dependencies ) } @@ -195,7 +203,7 @@ enum Onboarding { .sinkUntilComplete() } - func completeRegistration() { + func completeRegistration(using dependencies: Dependencies) { // Set the `lastNameUpdate` to the current date, so that we don't overwrite // what the user set in the display name step with whatever we find in their // swarm (otherwise the user could enter a display name and have it immediately @@ -205,7 +213,8 @@ enum Onboarding { .filter(id: getUserHexEncodedPublicKey(db)) .updateAllAndConfig( db, - Profile.Columns.lastNameUpdate.set(to: Date().timeIntervalSince1970) + Profile.Columns.lastNameUpdate.set(to: Date().timeIntervalSince1970), + using: dependencies ) } diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index 703499ef52a..ec9a8addec9 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -160,7 +160,7 @@ struct PNModeScreen: View { } private func finishRegister() { - self.flow.completeRegistration() + self.flow.completeRegistration(using: dependencies) let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 09b71135d12..5cf0f9d2372 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -10,6 +10,7 @@ import SessionUtilitiesKit import SignalUtilitiesKit final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate, NavigatableStateHolder { + private let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() private var disposables: Set = Set() @@ -55,6 +56,18 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC return result }() + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: - Lifecycle @@ -185,7 +198,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC isJoining = true - ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in + ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self, dependencies] _ in Storage.shared .writePublisher { db in OpenGroupManager.shared.add( @@ -193,7 +206,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC roomToken: roomToken, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .flatMap { successfullyAddedGroup in @@ -240,7 +253,8 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC for: OpenGroup.idFor(roomToken: roomToken, server: server), variant: .community, dismissing: nil, - animated: false + animated: false, + using: dependencies ) } } diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index 764b55cc8fd..d2dbfef6613 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -205,12 +205,16 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo confirmTitle: "blockUnblock".localized(), confirmStyle: .danger, cancelStyle: .alert_text - ) { [weak self] _ in + ) { [weak self, dependencies] _ in // Unblock the contacts Storage.shared.write { db in _ = try Contact .filter(ids: contactIds) - .updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false)) + .updateAllAndConfig( + db, + Contact.Columns.isBlocked.set(to: false), + using: dependencies + ) } self?.selectedContactIdsSubject.send([]) diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift new file mode 100644 index 00000000000..f172b7361ef --- /dev/null +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -0,0 +1,599 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import CryptoKit +import Compression +import GRDB +import DifferenceKit +import SessionUIKit +import SessionSnodeKit +import SessionMessagingKit +import SessionUtilitiesKit +import SignalUtilitiesKit + +class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + private var databaseKeyEncryptionPassword: String = "" + private var documentPickerResult: DocumentPickerResult? + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + // MARK: - Section + + public enum Section: SessionTableSection { + case developerMode + case database + + var title: String? { + switch self { + case .developerMode: return nil + case .database: return "Database" + } + } + + var style: SessionTableSectionStyle { + switch self { + case .developerMode: return .padding + default: return .titleRoundedContent + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case developerMode + + case exportDatabase + case importDatabase + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .developerMode: return "developerMode" + + case .exportDatabase: return "exportDatabase" + case .importDatabase: return "importDatabase" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.developerMode { + case .developerMode: result.append(.developerMode); fallthrough + + case .exportDatabase: result.append(.exportDatabase); fallthrough + case .importDatabase: result.append(.importDatabase) + } + + return result + } + } + + // MARK: - Content + + private struct State: Equatable { + let developerMode: Bool + } + + let title: String = "Developer Settings" + + lazy var observation: TargetObservation = ObservationBuilder + .refreshableData(self) { [weak self, dependencies] () -> State in + State( + developerMode: dependencies.storage[.developerModeEnabled] + ) + } + .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } + + private func content(_ previous: State?, _ current: State) -> [SectionModel] { + return [ + SectionModel( + model: .developerMode, + elements: [ + SessionCell.Info( + id: .developerMode, + title: "Developer Mode", + subtitle: """ + Grants access to this screen. + + Disabling this setting will: + • Reset all the below settings to default (removing data as described below) + • Revoke access to this screen unless Developer Mode is re-enabled + """, + rightAccessory: .toggle( + .boolValue( + current.developerMode, + oldValue: (previous?.developerMode == true) + ) + ), + onTap: { [weak self] in + guard current.developerMode else { return } + + self?.disableDeveloperMode() + } + ) + ] + ), + SectionModel( + model: .database, + elements: [ + SessionCell.Info( + id: .exportDatabase, + title: "Export App Data", + rightAccessory: .icon( + UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")? + .withRenderingMode(.alwaysTemplate), + size: .small + ), + styling: SessionCell.StyleInfo( + tintColor: .danger + ), + onTapView: { [weak self] view in self?.exportDatabase(view) } + ), + SessionCell.Info( + id: .importDatabase, + title: "Import App Data", + rightAccessory: .icon( + UIImage(systemName: "square.and.arrow.down")? + .withRenderingMode(.alwaysTemplate), + size: .small + ), + styling: SessionCell.StyleInfo( + tintColor: .danger + ), + onTapView: { [weak self] view in self?.importDatabase(view) } + ) + ] + ) + ] + } + + // MARK: - Functions + + private func disableDeveloperMode() { + /// Loop through all of the sections and reset the features back to default for each one as needed (this way if a new section is added + /// then we will get a compile error if it doesn't get resetting instructions added) + TableItem.allCases.forEach { item in + switch item { + case .developerMode: break // Not a feature + + case .exportDatabase: break // Not a feature + case .importDatabase: break // Not a feature + } + } + + /// Disable developer mode + dependencies.storage.write { db in + db[.developerModeEnabled] = false + } + + self.dismissScreen(type: .pop) + } + + // MARK: - Export and Import + + private func exportDatabase(_ targetView: UIView?) { + let generatedPassword: String = UUID().uuidString + self.databaseKeyEncryptionPassword = generatedPassword + + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Export App Data", + body: .input( + explanation: NSAttributedString( + string: """ + This will generate a file encrypted using the provided password includes all app data, attachments, settings and keys. + + This exported file can only be imported by Session iOS. + + Use at your own risk! + + We've generated a secure password for you but feel free to provide your own. + """ + ), + placeholder: "Enter a password", + initialValue: generatedPassword, + clearButton: true, + onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } + ), + confirmTitle: "save".localized(), + confirmStyle: .alert_text, + cancelTitle: "share".localized(), + cancelStyle: .alert_text, + hasCloseButton: true, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + modal.dismiss(animated: true) { + self?.performExport(viaShareSheet: false, targetView: targetView) + } + }, + onCancel: { [weak self] modal in + modal.dismiss(animated: true) { + self?.performExport(viaShareSheet: true, targetView: targetView) + } + } + ) + ), + transitionType: .present + ) + } + + private func importDatabase(_ targetView: UIView?) { + self.databaseKeyEncryptionPassword = "" + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Import App Data", + body: .input( + explanation: NSAttributedString( + string: """ + Importing a database will result in the loss of all data stored locally. + + This can only import backup files exported by Session iOS. + + Use at your own risk! + """ + ), + placeholder: "Enter a password", + initialValue: "", + clearButton: true, + onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } + ), + confirmTitle: "Import", + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + modal.dismiss(animated: true) { + self?.performImport() + } + } + ) + ), + transitionType: .present + ) + } + + private func performExport( + viaShareSheet: Bool, + targetView: UIView? + ) { + func showError(_ error: Error) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: { + switch error { + case CryptoKitError.incorrectKeySize: + return .text("The password must be between 6 and 32 characters (padded to 32 bytes)") + case is DatabaseError: + return .text("An error occurred finalising pending changes in the database") + + default: return .text("Failed to export database") + } + }() + ) + ), + transitionType: .present + ) + } + guard databaseKeyEncryptionPassword.count >= 6 else { return showError(CryptoKitError.incorrectKeySize) } + guard Singleton.hasAppContext else { return showError(CryptoKitError.incorrectParameterSize) } + + let viewController: UIViewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, databaseKeyEncryptionPassword, dependencies] modalActivityIndicator in + let backupFile: String = "\(Singleton.appContext.temporaryDirectory)/session.bak" + + do { + /// Perform a full checkpoint to ensure any pending changes are written to the main database file + try dependencies.storage.checkpoint(.truncate) + + let secureDbKey: String = try dependencies.storage.secureExportKey( + password: databaseKeyEncryptionPassword + ) + + try DirectoryArchiver.archiveDirectory( + sourcePath: FileManager.default.appSharedDataDirectoryPath, + destinationPath: backupFile, + filenamesToExclude: [ + ".DS_Store", + "\(Storage.dbFileName)-wal", + "\(Storage.dbFileName)-shm" + ], + additionalPaths: [secureDbKey], + password: databaseKeyEncryptionPassword, + progressChanged: { fileIndex, totalFiles, currentFileProgress, currentFileSize in + let percentage: Int = { + guard currentFileSize > 0 else { return 100 } + + let percentage: Int = Int((Double(currentFileProgress) / Double(currentFileSize)) * 100) + + guard percentage > 0 else { return 100 } + + return percentage + }() + + DispatchQueue.main.async { + modalActivityIndicator.setMessage([ + "Exporting file: \(fileIndex)/\(totalFiles)", + "File encryption progress: \(percentage)%" + ].compactMap { $0 }.joined(separator: "\n")) + } + } + ) + } + catch { + modalActivityIndicator.dismiss { + showError(error) + } + return + } + + modalActivityIndicator.dismiss { + switch viaShareSheet { + case true: + let shareVC: UIActivityViewController = UIActivityViewController( + activityItems: [ URL(fileURLWithPath: backupFile) ], + applicationActivities: nil + ) + shareVC.completionWithItemsHandler = { _, _, _, _ in } + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + shareVC.popoverPresentationController?.sourceView = targetView + shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) + } + + self?.transitionToScreen(shareVC, transitionType: .present) + + case false: + // Create and present the document picker + let documentPickerResult: DocumentPickerResult = DocumentPickerResult { _ in } + self?.documentPickerResult = documentPickerResult + + let documentPicker: UIDocumentPickerViewController = UIDocumentPickerViewController( + forExporting: [URL(fileURLWithPath: backupFile)] + ) + documentPicker.delegate = documentPickerResult + documentPicker.modalPresentationStyle = .formSheet + self?.transitionToScreen(documentPicker, transitionType: .present) + } + } + } + + self.transitionToScreen(viewController, transitionType: .present) + } + + private func performImport() { + func showError(_ error: Error) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: { + switch error { + case CryptoKitError.incorrectKeySize: + return .text("The password must be between 6 and 32 characters (padded to 32 bytes)") + + case is DatabaseError: return .text("Database key in backup file was invalid.") + default: return .text("\(error)") + } + }() + ) + ), + transitionType: .present + ) + } + + guard databaseKeyEncryptionPassword.count >= 6 else { return showError(CryptoKitError.incorrectKeySize) } + + let documentPickerResult: DocumentPickerResult = DocumentPickerResult { url in + guard let url: URL = url else { return } + + let viewController: UIViewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, password = self.databaseKeyEncryptionPassword, dependencies = self.dependencies] modalActivityIndicator in + do { + let tmpUnencryptPath: String = "\(Singleton.appContext.temporaryDirectory)/new_session.bak" + let (paths, additionalFilePaths): ([String], [String]) = try DirectoryArchiver.unarchiveDirectory( + archivePath: url.path, + destinationPath: tmpUnencryptPath, + password: password, + progressChanged: { filesSaved, totalFiles, fileProgress, fileSize in + let percentage: Int = { + guard fileSize > 0 else { return 0 } + + return Int((Double(fileProgress) / Double(fileSize)) * 100) + }() + + DispatchQueue.main.async { + modalActivityIndicator.setMessage([ + "Decryption progress: \(percentage)%", + "Files imported: \(filesSaved)/\(totalFiles)" + ].compactMap { $0 }.joined(separator: "\n")) + } + } + ) + + /// Test that we actually have valid access to the database + guard + let encKeyPath: String = additionalFilePaths + .first(where: { $0.hasSuffix(Storage.encKeyFilename) }), + let databasePath: String = paths + .first(where: { $0.hasSuffix(Storage.dbFileName) }) + else { throw ArchiveError.unableToFindDatabaseKey } + + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Checking for valid database..." + ) + } + + let testStorage: Storage = try Storage( + testAccessTo: databasePath, + encryptedKeyPath: encKeyPath, + encryptedKeyPassword: password + ) + + guard testStorage.isValid else { + throw ArchiveError.decryptionFailed + } + + /// Now that we have confirmed access to the replacement database we need to + /// stop the current account from doing anything + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Clearing current account data..." + ) + + (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() + } + + dependencies.jobRunner.stopAndClearPendingJobs(using: dependencies) + LibSession.suspendNetworkAccess() + dependencies.storage.suspendDatabaseAccess() + try dependencies.storage.closeDatabase() + + let deleteEnumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( + at: URL( + fileURLWithPath: FileManager.default.appSharedDataDirectoryPath + ), + includingPropertiesForKeys: [.isRegularFileKey] + ) + let fileUrls: [URL] = (deleteEnumerator?.allObjects.compactMap { $0 as? URL } ?? []) + try fileUrls.forEach { url in + /// The database `wal` and `shm` files might not exist anymore at this point + /// so we should only remove files which exist to prevent errors + guard FileManager.default.fileExists(atPath: url.path) else { return } + + try FileManager.default.removeItem(atPath: url.path) + } + + /// Current account data has been removed, we now need to copy over the + /// newly imported data + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Moving imported data..." + ) + } + + try paths.forEach { path in + /// Need to ensure the destination directry + let targetPath: String = [ + FileManager.default.appSharedDataDirectoryPath, + path.replacingOccurrences(of: tmpUnencryptPath, with: "") + ].joined() // Already has '/' after 'appSharedDataDirectoryPath' + + try FileManager.default.createDirectory( + atPath: URL(fileURLWithPath: targetPath) + .deletingLastPathComponent() + .path, + withIntermediateDirectories: true + ) + try FileManager.default.moveItem(atPath: path, toPath: targetPath) + } + + /// All of the main files have been moved across, we now need to replace the current database key with + /// the one included in the backup + try dependencies.storage.replaceDatabaseKey(path: encKeyPath, password: password) + + /// The import process has completed so we need to restart the app + DispatchQueue.main.async { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Import Complete", + body: .text("The import completed successfully, Session must be reopened in order to complete the process."), + cancelTitle: "Exit", + cancelStyle: .alert_text, + onCancel: { _ in exit(0) } + ) + ), + transitionType: .present + ) + } + } + catch { + modalActivityIndicator.dismiss { + showError(error) + } + } + } + + self.transitionToScreen(viewController, transitionType: .present) + } + self.documentPickerResult = documentPickerResult + + // UIDocumentPickerModeImport copies to a temp file within our container. + // It uses more memory than "open" but lets us avoid working with security scoped URLs. + let documentPickerVC = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true) + documentPickerVC.delegate = documentPickerResult + documentPickerVC.modalPresentationStyle = .fullScreen + + self.transitionToScreen(documentPickerVC, transitionType: .present) + } +} + +// MARK: - Automated Test Convenience + +extension DeveloperSettingsViewModel { + static func processUnitTestEnvVariablesIfNeeded() { +#if targetEnvironment(simulator) + enum EnvironmentVariable: String { + case animationsEnabled + } + + ProcessInfo.processInfo.environment.forEach { key, value in + guard let variable: EnvironmentVariable = EnvironmentVariable(rawValue: key) else { return } + + switch variable { + case .animationsEnabled: + guard value == "false" else { return } + + UIView.setAnimationsEnabled(false) + } + } +#endif + } +} + +// MARK: - DocumentPickerResult + +private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { + private let onResult: (URL?) -> Void + + init(onResult: @escaping (URL?) -> Void) { + self.onResult = onResult + } + + // MARK: - UIDocumentPickerDelegate + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url: URL = urls.first else { + self.onResult(nil) + return + } + + self.onResult(url) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + self.onResult(nil) + } +} diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index e8fff044134..2530fd329d9 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -16,10 +16,6 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() -#if DEBUG - private var databaseKeyEncryptionPassword: String = "" -#endif - // MARK: - Initialization init(using dependencies: Dependencies = Dependencies()) { @@ -34,9 +30,6 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa case feedback case faq case support -#if DEBUG - case exportDatabase -#endif var style: SessionTableSectionStyle { .padding } } @@ -147,33 +140,9 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa } ) ] - ), - maybeExportDbSection + ) ] -#if DEBUG - private lazy var maybeExportDbSection: SectionModel? = SectionModel( - model: .exportDatabase, - elements: [ - SessionCell.Info( - id: .support, - title: "Export Database", // stringlint:ignore - rightAccessory: .icon( - UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")? - .withRenderingMode(.alwaysTemplate), - size: .small - ), - styling: SessionCell.StyleInfo( - tintColor: .danger - ), - onTapView: { [weak self] view in self?.exportDatabase(view) } - ) - ] - ) -#else - private let maybeExportDbSection: SectionModel? = nil -#endif - // MARK: - Functions public static func shareLogs( @@ -262,133 +231,4 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa showShareSheet() } } - -#if DEBUG - // stringlint:ignore_contents - private func exportDatabase(_ targetView: UIView?) { - let generatedPassword: String = UUID().uuidString - self.databaseKeyEncryptionPassword = generatedPassword - - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Export Database", - body: .input( - explanation: NSAttributedString( - string: """ - Sharing the database and key together is dangerous! - - We've generated a secure password for you but feel free to provide your own (we will show the generated password again after exporting) - - This password will be used to encrypt the database decryption key and will be exported alongside the database - """ - ), - placeholder: "Enter a password", - initialValue: generatedPassword, - clearButton: true, - onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } - ), - confirmTitle: "Export", - dismissOnConfirm: false, - onConfirm: { [weak self] modal in - modal.dismiss(animated: true) { - guard let password: String = self?.databaseKeyEncryptionPassword, password.count >= 6 else { - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Error", - body: .text("Password must be at least 6 characters") - ) - ), - transitionType: .present - ) - return - } - - do { - let exportInfo = try Storage.shared.exportInfo(password: password) - let shareVC = UIActivityViewController( - activityItems: [ - URL(fileURLWithPath: exportInfo.dbPath), - URL(fileURLWithPath: exportInfo.keyPath) - ], - applicationActivities: nil - ) - shareVC.completionWithItemsHandler = { [weak self] _, completed, _, _ in - guard - completed && - generatedPassword == self?.databaseKeyEncryptionPassword - else { return } - - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Password", - body: .text(""" - The generated password was: - \(generatedPassword) - - Avoid sending this via the same means as the database - """), - confirmTitle: "Share", - dismissOnConfirm: false, - onConfirm: { [weak self] modal in - modal.dismiss(animated: true) { - let passwordShareVC = UIActivityViewController( - activityItems: [generatedPassword], - applicationActivities: nil - ) - if UIDevice.current.isIPad { - passwordShareVC.excludedActivityTypes = [] - passwordShareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - passwordShareVC.popoverPresentationController?.sourceView = targetView - passwordShareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) - } - - self?.transitionToScreen(passwordShareVC, transitionType: .present) - } - } - ) - ), - transitionType: .present - ) - } - - if UIDevice.current.isIPad { - shareVC.excludedActivityTypes = [] - shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - shareVC.popoverPresentationController?.sourceView = targetView - shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) - } - - self?.transitionToScreen(shareVC, transitionType: .present) - } - catch { - let message: String = { - switch error { - case CryptoKitError.incorrectKeySize: - return "The password must be between 6 and 32 characters (padded to 32 bytes)" - - default: return "Failed to export database" - } - }() - - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Error", - body: .text(message) - ) - ), - transitionType: .present - ) - } - } - } - ) - ), - transitionType: .present - ) - } -#endif } diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 181622fb78c..ddb279b7c18 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -139,7 +139,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav } Storage.shared.write { db in - try db.setAndUpdateConfig(.isScreenLockEnabled, to: !db[.isScreenLockEnabled]) + try db.setAndUpdateConfig( + .isScreenLockEnabled, + to: !db[.isScreenLockEnabled], + using: dependencies + ) } } ) @@ -166,7 +170,8 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav Storage.shared.write { db in try db.setAndUpdateConfig( .checkForCommunityMessageRequests, - to: !db[.checkForCommunityMessageRequests] + to: !db[.checkForCommunityMessageRequests], + using: dependencies ) } } @@ -192,7 +197,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.areReadReceiptsEnabled, to: !db[.areReadReceiptsEnabled]) + try db.setAndUpdateConfig( + .areReadReceiptsEnabled, + to: !db[.areReadReceiptsEnabled], + using: dependencies + ) } } ) @@ -252,7 +261,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.typingIndicatorsEnabled, to: !db[.typingIndicatorsEnabled]) + try db.setAndUpdateConfig( + .typingIndicatorsEnabled, + to: !db[.typingIndicatorsEnabled], + using: dependencies + ) } } ) @@ -277,7 +290,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.areLinkPreviewsEnabled, to: !db[.areLinkPreviewsEnabled]) + try db.setAndUpdateConfig( + .areLinkPreviewsEnabled, + to: !db[.areLinkPreviewsEnabled], + using: dependencies + ) } } ) @@ -314,7 +331,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.areCallsEnabled, to: !db[.areCallsEnabled]) + try db.setAndUpdateConfig( + .areCallsEnabled, + to: !db[.areCallsEnabled], + using: dependencies + ) } } ) diff --git a/Session/Settings/QRCodeScreen.swift b/Session/Settings/QRCodeScreen.swift index c7d59fe4efc..53093366063 100644 --- a/Session/Settings/QRCodeScreen.swift +++ b/Session/Settings/QRCodeScreen.swift @@ -8,11 +8,16 @@ import AVFoundation struct QRCodeScreen: View { @EnvironmentObject var host: HostWrapper + let dependencies: Dependencies @State var tabIndex = 0 @State private var accountId: String = "" @State private var errorString: String? = nil + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + var body: some View { ZStack(alignment: .topLeading) { VStack( @@ -51,7 +56,8 @@ struct QRCodeScreen: View { variant: .contact, action: .compose, dismissing: self.host.controller, - animated: false + animated: false, + using: dependencies ) } } @@ -94,5 +100,5 @@ struct MyQRCodeScreen: View { } #Preview { - QRCodeScreen() + QRCodeScreen(using: Dependencies()) } diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 003900c7ed0..b96cafa58f7 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -90,6 +90,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl case inviteAFriend case recoveryPhrase case help + case developerSettings case clearData } @@ -145,7 +146,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .eraseToAnyPublisher() lazy var rightNavItems: AnyPublisher<[SessionNavItem], Never> = navState - .map { [weak self] navState -> [SessionNavItem] in + .map { [weak self, dependencies] navState -> [SessionNavItem] in switch navState { case .standard: return [ @@ -156,7 +157,9 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl style: .plain, accessibilityIdentifier: "View QR code", action: { [weak self] in - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: QRCodeScreen()) + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: QRCodeScreen(using: dependencies) + ) viewController.setNavBarTitle("qrCode".localized()) self?.transitionToScreen(viewController) } @@ -210,8 +213,10 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .eraseToAnyPublisher() // MARK: - Content + private struct State: Equatable { let profile: Profile + let developerModeEnabled: Bool let hideRecoveryPasswordPermanently: Bool } @@ -221,217 +226,217 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .databaseObservation(self) { [weak self, dependencies] db -> State in State( profile: Profile.fetchOrCreateCurrentUser(db, using: dependencies), + developerModeEnabled: db[.developerModeEnabled], hideRecoveryPasswordPermanently: db[.hideRecoveryPasswordPermanently] ) } - .map { [weak self] state -> [SectionModel] in - return [ - SectionModel( - model: .profileInfo, - elements: [ - SessionCell.Info( - id: .avatar, - accessory: .profile( - id: state.profile.id, - size: .hero, - profile: state.profile - ), - styling: SessionCell.StyleInfo( - alignment: .centerHugging, - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), - backgroundStyle: .noBackground - ), + .map { [weak self, dependencies] state -> [SectionModel] in + let profileInfo: SectionModel = SectionModel( + model: .profileInfo, + elements: [ + SessionCell.Info( + id: .avatar, + accessory: .profile( + id: state.profile.id, + size: .hero, + profile: state.profile + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "User settings", + label: "Profile picture" + ), + onTap: { + self?.updateProfilePicture(currentFileName: state.profile.profilePictureFileName) + } + ), + SessionCell.Info( + id: .profileName, + title: SessionCell.TextInfo( + state.profile.displayName(), + font: .titleLarge, + alignment: .center, + interaction: .editable + ), + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding(top: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Username", + label: state.profile.displayName() + ), + onTap: { self?.setIsEditing(true) } + ) + ] + ) + let sessionId: SectionModel = SectionModel( + model: .sessionId, + elements: [ + SessionCell.Info( + id: .sessionId, + title: SessionCell.TextInfo( + state.profile.id, + font: .monoLarge, + alignment: .center, + interaction: .copy + ), + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Account ID", + label: state.profile.id + ) + ), + SessionCell.Info( + id: .idActions, + leftAccessory: .button( + style: .bordered, + title: "share".localized(), accessibility: Accessibility( - identifier: "User settings", - label: "Profile picture" + identifier: "Share button", + label: "Share button" ), - onTap: { - self?.updateProfilePicture(currentFileName: state.profile.profilePictureFileName) + run: { _ in + self?.shareSessionId(state.profile.id) } ), - SessionCell.Info( - id: .profileName, - title: SessionCell.TextInfo( - state.profile.displayName(), - font: .titleLarge, - alignment: .center, - interaction: .editable - ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding(top: Values.smallSpacing), - backgroundStyle: .noBackground - ), + rightAccessory: .button( + style: .bordered, + title: "copy".localized(), accessibility: Accessibility( - identifier: "Username", - label: state.profile.displayName() - ), - onTap: { self?.setIsEditing(true) } - ) - ] - ), - SectionModel( - model: .sessionId, - elements: [ - SessionCell.Info( - id: .sessionId, - title: SessionCell.TextInfo( - state.profile.id, - font: .monoLarge, - alignment: .center, - interaction: .copy - ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), - backgroundStyle: .noBackground + identifier: "Copy button", + label: "Copy button" ), - accessibility: Accessibility( - identifier: "Account ID", - label: state.profile.id - ) + run: { button in + self?.copySessionId(state.profile.id, button: button) + } ), - SessionCell.Info( - id: .idActions, - leftAccessory: .button( - style: .bordered, - title: "share".localized(), - accessibility: Accessibility( - identifier: "Share button", - label: "Share button" - ), - run: { _ in - self?.shareSessionId(state.profile.id) - } - ), - rightAccessory: .button( - style: .bordered, - title: "copy".localized(), - accessibility: Accessibility( - identifier: "Copy button", - label: "Copy button" - ), - run: { button in - self?.copySessionId(state.profile.id, button: button) - } + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + leading: 0, + trailing: 0 ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding( - top: Values.smallSpacing, - leading: 0, - trailing: 0 - ), - backgroundStyle: .noBackground - ) + backgroundStyle: .noBackground ) - ] - ), - SectionModel( - model: .menus, - elements: [ - SessionCell.Info( - id: .path, - leftAccessory: .customView(hashValue: "PathStatusView") { // stringlint:ignore - // Need to ensure this view is the same size as the icons so - // wrap it in a larger view - let result: UIView = UIView() - let pathView: PathStatusView = PathStatusView(size: .large) - result.addSubview(pathView) - - result.set(.width, to: IconSize.medium.size) - result.set(.height, to: IconSize.medium.size) - pathView.center(in: result) - - return result - }, - title: "onionRoutingPath".localized(), - onTap: { self?.transitionToScreen(PathVC()) } + ) + ] + ) + let menus: SectionModel = SectionModel( + model: .menus, + elements: [ + SessionCell.Info( + id: .path, + leftAccessory: .customView(hashValue: "PathStatusView") { // stringlint:ignore + // Need to ensure this view is the same size as the icons so + // wrap it in a larger view + let result: UIView = UIView() + let pathView: PathStatusView = PathStatusView(size: .large) + result.addSubview(pathView) + + result.set(.width, to: IconSize.medium.size) + result.set(.height, to: IconSize.medium.size) + pathView.center(in: result) + + return result + }, + title: "onionRoutingPath".localized(), + onTap: { self?.transitionToScreen(PathVC()) } + ), + SessionCell.Info( + id: .privacy, + leftAccessory: .icon( + UIImage(named: "icon_privacy")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .privacy, - leftAccessory: .icon( - UIImage(named: "icon_privacy")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionPrivacy".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: PrivacySettingsViewModel()) - ) - } + title: "sessionPrivacy".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: PrivacySettingsViewModel()) + ) + } + ), + SessionCell.Info( + id: .notifications, + leftAccessory: .icon( + UIImage(named: "icon_speaker")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .notifications, - leftAccessory: .icon( - UIImage(named: "icon_speaker")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionNotifications".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: NotificationSettingsViewModel()) - ) - } + title: "sessionNotifications".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: NotificationSettingsViewModel()) + ) + } + ), + SessionCell.Info( + id: .conversations, + leftAccessory: .icon( + UIImage(named: "icon_msg")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .conversations, - leftAccessory: .icon( - UIImage(named: "icon_msg")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionConversations".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: ConversationSettingsViewModel()) - ) - } + title: "sessionConversations".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: ConversationSettingsViewModel()) + ) + } + ), + SessionCell.Info( + id: .messageRequests, + leftAccessory: .icon( + UIImage(named: "icon_msg_req")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .messageRequests, - leftAccessory: .icon( - UIImage(named: "icon_msg_req")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionMessageRequests".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: MessageRequestsViewModel()) - ) - } + title: "sessionMessageRequests".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: MessageRequestsViewModel()) + ) + } + ), + SessionCell.Info( + id: .appearance, + leftAccessory: .icon( + UIImage(named: "icon_apperance")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .appearance, - leftAccessory: .icon( - UIImage(named: "icon_apperance")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionAppearance".localized(), - onTap: { - self?.transitionToScreen(AppearanceViewController()) - } + title: "sessionAppearance".localized(), + onTap: { + self?.transitionToScreen(AppearanceViewController()) + } + ), + SessionCell.Info( + id: .inviteAFriend, + leftAccessory: .icon( + UIImage(named: "icon_invite")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .inviteAFriend, - leftAccessory: .icon( - UIImage(named: "icon_invite")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionInviteAFriend".localized(), - onTap: { - let invitation: String = "accountIdShare" - .put(key: "app_name", value: Constants.app_name) - .put(key: "account_id", value: state.profile.id) - .put(key: "session_download_url", value: Constants.session_download_url) - .localized() - - self?.transitionToScreen( - UIActivityViewController( - activityItems: [ invitation ], - applicationActivities: nil - ), - transitionType: .present - ) - } - ) - ].appending( + title: "sessionInviteAFriend".localized(), + onTap: { + let invitation: String = "accountIdShare" + .put(key: "app_name", value: Constants.app_name) + .put(key: "account_id", value: state.profile.id) + .put(key: "session_download_url", value: Constants.session_download_url) + .localized() + + self?.transitionToScreen( + UIActivityViewController( + activityItems: [ invitation ], + applicationActivities: nil + ), + transitionType: .present + ) + } + ), + ( state.hideRecoveryPasswordPermanently ? nil : SessionCell.Info( id: .recoveryPhrase, @@ -462,43 +467,66 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } } ) - ).appending(contentsOf: [ + ), + SessionCell.Info( + id: .help, + leftAccessory: .icon( + UIImage(named: "icon_help")? + .withRenderingMode(.alwaysTemplate) + ), + title: "sessionHelp".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: HelpViewModel()) + ) + } + ), + (!state.developerModeEnabled ? nil : SessionCell.Info( - id: .help, + id: .developerSettings, leftAccessory: .icon( - UIImage(named: "icon_help")? + UIImage(systemName: "wrench.and.screwdriver")? .withRenderingMode(.alwaysTemplate) ), - title: "sessionHelp".localized(), + title: "Developer Settings", // stringlint:ignore + styling: SessionCell.StyleInfo(tintColor: .warning), onTap: { self?.transitionToScreen( - SessionTableViewController(viewModel: HelpViewModel()) + SessionTableViewController(viewModel: DeveloperSettingsViewModel(using: dependencies)) ) } - ), - SessionCell.Info( - id: .clearData, - leftAccessory: .icon( - UIImage(named: "icon_bin")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionClearData".localized(), - styling: SessionCell.StyleInfo(tintColor: .danger), - onTap: { - self?.transitionToScreen(NukeDataModal(), transitionType: .present) - } ) - ]) - ) - ] + ), + SessionCell.Info( + id: .clearData, + leftAccessory: .icon( + UIImage(named: "icon_bin")? + .withRenderingMode(.alwaysTemplate) + ), + title: "sessionClearData".localized(), + styling: SessionCell.StyleInfo(tintColor: .danger), + onTap: { + self?.transitionToScreen(NukeDataModal(), transitionType: .present) + } + ) + ].compactMap { $0 } + ) + + return [profileInfo, sessionId, menus] } - public let footerView: AnyPublisher = Just(VersionFooterView()).eraseToAnyPublisher() + public lazy var footerView: AnyPublisher = Just(VersionFooterView(numTaps: 9) { [dependencies] in + /// Do nothing if developer mode is already enabled + guard !dependencies.storage[.developerModeEnabled] else { return } + + dependencies.storage.write { db in + db[.developerModeEnabled] = true + } + }).eraseToAnyPublisher() // MARK: - Functions private func updateProfilePicture(currentFileName: String?) { - let existingDisplayName: String = self.oldDisplayName let existingImageData: Data? = ProfileManager .profileAvatar(id: self.userSessionId) let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info( diff --git a/Session/Settings/Views/VersionFooterView.swift b/Session/Settings/Views/VersionFooterView.swift index fa0eb801ce1..539f94d9cf1 100644 --- a/Session/Settings/Views/VersionFooterView.swift +++ b/Session/Settings/Views/VersionFooterView.swift @@ -7,6 +7,8 @@ class VersionFooterView: UIView { private static let footerHeight: CGFloat = 75 private static let logoHeight: CGFloat = 24 + private let multiTapCallback: (() -> Void)? + // MARK: - UI private lazy var logoImageView: UIImageView = { @@ -38,23 +40,25 @@ class VersionFooterView: UIView { let version: String = ((infoDict?["CFBundleShortVersionString"] as? String) ?? "0.0.0") let buildNumber: String? = (infoDict?["CFBundleVersion"] as? String) let commitInfo: String? = (infoDict?["GitCommitHash"] as? String) - let buildInfo: String = [buildNumber, commitInfo] + let buildInfo: String? = [buildNumber, commitInfo] .compactMap { $0 } .joined(separator: " - ") + .nullIfEmpty + .map { "(\($0))" } // stringlint:ignore_stop result.text = [ "Version \(version)", - (!buildInfo.isEmpty ? " (" : ""), - buildInfo, - (!buildInfo.isEmpty ? ")" : ""), - ].joined() + buildInfo + ].compactMap { $0 }.joined(separator: " ") return result }() // MARK: - Initialization - init() { + init(numTaps: Int = 0, multiTapCallback: (() -> Void)? = nil) { + self.multiTapCallback = multiTapCallback + // Note: Need to explicitly set the height for a table footer view // or it will have no height super.init( @@ -66,7 +70,7 @@ class VersionFooterView: UIView { ) ) - setupViewHierarchy() + setupViewHierarchy(numTaps: numTaps) } required init?(coder: NSCoder) { @@ -75,7 +79,7 @@ class VersionFooterView: UIView { // MARK: - Content - private func setupViewHierarchy() { + private func setupViewHierarchy(numTaps: Int) { addSubview(logoImageView) addSubview(versionLabel) @@ -84,5 +88,18 @@ class VersionFooterView: UIView { versionLabel.pin(.top, to: .bottom, of: logoImageView, withInset: Values.mediumSpacing) versionLabel.pin(.left, to: .left, of: self) versionLabel.pin(.right, to: .right, of: self) + + if numTaps > 0 { + let tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(onMultiTap) + ) + tapGestureRecognizer.numberOfTapsRequired = numTaps + addGestureRecognizer(tapGestureRecognizer) + } + } + + @objc private func onMultiTap() { + self.multiTapCallback?() } } diff --git a/Session/Shared/Types/ObservableTableSource.swift b/Session/Shared/Types/ObservableTableSource.swift index 006a9dd5d7e..5283e5b78ba 100644 --- a/Session/Shared/Types/ObservableTableSource.swift +++ b/Session/Shared/Types/ObservableTableSource.swift @@ -22,6 +22,11 @@ public protocol ObservableTableSource: AnyObject, SectionedTableData { func didReturnFromBackground() } +public enum ObservableTableSourceRefreshType { + case databaseQuery + case postDatabaseQuery +} + extension ObservableTableSource { public var pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never> { self.observableState.pendingTableDataSubject @@ -33,25 +38,33 @@ extension ObservableTableSource { public var tableDataPublisher: TargetPublisher { self.observation.finalPublisher(self, using: dependencies) } public func didReturnFromBackground() {} - public func forceRefresh() { self.observableState._forcedRefresh.send(()) } + public func forceRefresh(type: ObservableTableSourceRefreshType = .databaseQuery) { + switch type { + case .databaseQuery: self.observableState._forcedRequery.send(()) + case .postDatabaseQuery: self.observableState._forcedPostQueryRefresh.send(()) + } + } } // MARK: - State Manager (ObservableTableSource) public class ObservableTableSourceState: SectionedTableData { - public let forcedRefresh: AnyPublisher + fileprivate let forcedRequery: AnyPublisher + fileprivate let forcedPostQueryRefresh: AnyPublisher public let pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never> // MARK: - Internal Variables fileprivate var hasEmittedInitialData: Bool - fileprivate let _forcedRefresh: PassthroughSubject = PassthroughSubject() + fileprivate let _forcedRequery: PassthroughSubject = PassthroughSubject() + fileprivate let _forcedPostQueryRefresh: PassthroughSubject = PassthroughSubject() // MARK: - Initialization init() { self.hasEmittedInitialData = false - self.forcedRefresh = _forcedRefresh.shareReplay(0) + self.forcedRequery = _forcedRequery.shareReplay(0) + self.forcedPostQueryRefresh = _forcedPostQueryRefresh.shareReplay(0) self.pendingTableDataSubject = CurrentValueSubject([]) } } @@ -152,29 +165,67 @@ public enum ObservationBuilder { /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this return TableObservation { viewModel, dependencies in - return ValueObservation - .trackingConstantRegion(fetch) - .removeDuplicates() - .handleEvents(didFail: { SNLog("[\(type(of: viewModel))] Observation failed with error: \($0)") }) - .publisher(in: dependencies.storage, scheduling: dependencies.scheduler) - .manualRefreshFrom(source.observableState.forcedRefresh) + let subject: CurrentValueSubject = CurrentValueSubject(nil) + var forcedRefreshCancellable: AnyCancellable? + var observationCancellable: DatabaseCancellable? + + /// In order to force a `ValueObservation` to requery we need to resubscribe to it, as a result we create a + /// `CurrentValueSubject` and in the `receiveSubscription` call we start the `ValueObservation` sending + /// it's output into the subject + /// + /// **Note:** We need to use a `CurrentValueSubject` here because the `ValueObservation` could send it's + /// first value _before_ the subscription is properly setup, by using a `CurrentValueSubject` the value will be stored + /// and emitted once the subscription becomes valid + return subject + .compactMap { $0 } + .handleEvents( + receiveSubscription: { subscription in + forcedRefreshCancellable = source.observableState.forcedRequery + .prepend(()) + .sink( + receiveCompletion: { _ in }, + receiveValue: { _ in + /// Cancel any previous observation and create a brand new observation for this refresh + /// + /// **Note:** The `ValueObservation` **MUST** be started from the main thread + observationCancellable?.cancel() + observationCancellable = dependencies.storage.start( + ValueObservation + .trackingConstantRegion(fetch) + .removeDuplicates(), + scheduling: dependencies.scheduler, + onError: { error in + let log: String = [ + "[\(type(of: viewModel))]", // stringlint:ignore + "Observation failed with error:", // stringlint:ignore + "\(error)" // stringlint:ignore + ].joined(separator: " ") + SNLog(log) + subject.send(completion: Subscribers.Completion.failure(error)) + }, + onChange: { subject.send($0) } + ) + } + ) + }, + receiveCancel: { + forcedRefreshCancellable?.cancel() + observationCancellable?.cancel() + } + ) + .manualRefreshFrom(source.observableState.forcedPostQueryRefresh) + .shareReplay(1) // Share to prevent multiple subscribers resulting in multiple ValueObservations + .eraseToAnyPublisher() } } - /// The `ValueObserveration` will trigger whenever any of the data fetched in the closure is updated, please see the following link for tips - /// to help optimise performance https://github.com/groue/GRDB.swift#valueobservation-performance - static func databaseObservation(_ source: S, fetch: @escaping (Database) throws -> [T]) -> TableObservation<[T]> { - /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) - /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own - /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) - /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this + static func refreshableData(_ source: S, fetch: @escaping () -> T) -> TableObservation { return TableObservation { viewModel, dependencies in - return ValueObservation - .trackingConstantRegion(fetch) - .removeDuplicates() - .handleEvents(didFail: { SNLog("[\(type(of: viewModel))] Observation failed with error: \($0)") }) - .publisher(in: dependencies.storage, scheduling: dependencies.scheduler) - .manualRefreshFrom(source.observableState.forcedRefresh) + source.observableState.forcedRequery + .prepend(()) + .setFailureType(to: Error.self) + .map { _ in fetch() } + .manualRefreshFrom(source.observableState.forcedPostQueryRefresh) } } } diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index c490df3d67b..db822af0a47 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -49,8 +49,14 @@ enum MockDataGenerator { logProgress("", "Start") // First create the thread used to indicate that the mock data has been generated - _ = try? SessionThread - .fetchOrCreate(db, id: "MockDatabaseThread", variant: .contact, shouldBeVisible: false) + _ = try? SessionThread.upsert( + db, + id: "MockDatabaseThread", + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + calledFromConfig: nil, + using: dependencies + ) // MARK: - -- DM Thread @@ -74,13 +80,14 @@ enum MockDataGenerator { .randomElement(using: &dmThreadRandomGenerator) ?? 0) // Generate the thread - let thread: SessionThread = try! SessionThread - .fetchOrCreate( - db, - id: randomSessionId, - variant: .contact, - shouldBeVisible: true - ) + let thread: SessionThread = try! SessionThread.upsert( + db, + id: randomSessionId, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: nil, + using: dependencies + ) // Generate the contact let contact: Contact = try! Contact( @@ -186,13 +193,14 @@ enum MockDataGenerator { members.append(randomSessionId) } - let thread: SessionThread = try! SessionThread - .fetchOrCreate( - db, - id: randomGroupPublicKey, - variant: .legacyGroup, - shouldBeVisible: true - ) + let thread: SessionThread = try! SessionThread.upsert( + db, + id: randomGroupPublicKey, + variant: .legacyGroup, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: nil, + using: dependencies + ) _ = try! ClosedGroup( threadId: randomGroupPublicKey, name: groupName, @@ -316,13 +324,14 @@ enum MockDataGenerator { } // Create the open group model and the thread - let thread: SessionThread = try! SessionThread - .fetchOrCreate( - db, - id: randomGroupPublicKey, - variant: .community, - shouldBeVisible: true - ) + let thread: SessionThread = try! SessionThread.upsert( + db, + id: randomGroupPublicKey, + variant: .community, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: nil, + using: dependencies + ) _ = try! OpenGroup( server: serverName, roomToken: roomName, diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index eb46ad1d3ed..ee8be3e41fd 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -50,7 +50,8 @@ public extension UIContextualAction { tableView: UITableView, threadViewModel: SessionThreadViewModel, viewController: UIViewController?, - navigatableStateHolder: NavigatableStateHolder? + navigatableStateHolder: NavigatableStateHolder?, + using dependencies: Dependencies ) -> [UIContextualAction]? { guard !actions.isEmpty else { return nil } @@ -106,10 +107,11 @@ public extension UIContextualAction { case true: threadViewModel.markAsRead( target: .threadAndInteractions( interactionsBeforeInclusive: threadViewModel.interactionId - ) + ), + using: dependencies ) - case false: threadViewModel.markAsUnread() + case false: threadViewModel.markAsUnread(using: dependencies) } } completionHandler(true) @@ -140,9 +142,10 @@ public extension UIContextualAction { Storage.shared.writeAsync { db in try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .deleteContactConversationAndMarkHidden, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -186,9 +189,10 @@ public extension UIContextualAction { Storage.shared.writeAsync { db in try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .hideContactConversation, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -235,7 +239,8 @@ public extension UIContextualAction { .updateAllAndConfig( db, SessionThread.Columns.pinnedPriority - .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)) + .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)), + using: dependencies ) } } @@ -337,15 +342,20 @@ public extension UIContextualAction { .save(db) try Contact .filter(id: threadViewModel.threadId) - .updateAllAndConfig(db, contactChanges) + .updateAllAndConfig( + db, + contactChanges, + using: dependencies + ) // Blocked message requests should be deleted if threadIsMessageRequest { try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .deleteContactConversationAndMarkHidden, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -439,7 +449,8 @@ public extension UIContextualAction { db, type: deletionType, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } catch { DispatchQueue.main.async { @@ -542,8 +553,7 @@ public extension UIContextualAction { case (.group, _), (.legacyGroup, _): return .leaveGroupAsync - case (.contact, _): - return .hideContactConversationAndDeleteContent + case (.contact, _): return .deleteContactConversationAndMarkHidden } }() @@ -552,7 +562,8 @@ public extension UIContextualAction { db, type: deletionType, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } diff --git a/SessionMessagingKit/Calls/CallManagerProtocol.swift b/SessionMessagingKit/Calls/CallManagerProtocol.swift index dcfc31aa4b5..b1b3587c50f 100644 --- a/SessionMessagingKit/Calls/CallManagerProtocol.swift +++ b/SessionMessagingKit/Calls/CallManagerProtocol.swift @@ -2,11 +2,33 @@ import Foundation import CallKit +import SessionUtilitiesKit + +// MARK: - Singleton + +public extension Singleton { + // FIXME: This will be reworked to be part of dependencies in the Groups Rebuild branch + fileprivate static var _callManager: Atomic = Atomic(NoopSessionCallManager()) + static var callManager: CallManagerProtocol { _callManager.wrappedValue } + + static func setCallManager(_ callManager: CallManagerProtocol) { + _callManager = Atomic(callManager) + } +} + +// MARK: - CallManagerProtocol public protocol CallManagerProtocol { var currentCall: CurrentCallProtocol? { get set } + func setCurrentCall(_ call: CurrentCallProtocol?) + func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) func reportCurrentCallEnded(reason: CXCallEndedReason?) + func suspendDatabaseIfCallEndedInBackground() + + func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) + func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) + func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) func handleICECandidates(message: CallMessage, sdpMLineIndexes: [UInt32], sdpMids: [String]) diff --git a/SessionMessagingKit/Calls/CurrentCallProtocol.swift b/SessionMessagingKit/Calls/CurrentCallProtocol.swift index c37a26bdd6e..2b811b4be1b 100644 --- a/SessionMessagingKit/Calls/CurrentCallProtocol.swift +++ b/SessionMessagingKit/Calls/CurrentCallProtocol.swift @@ -3,14 +3,16 @@ import Foundation import GRDB import WebRTC +import SessionUtilitiesKit public protocol CurrentCallProtocol { var uuid: String { get } var callId: UUID { get } + var sessionId: String { get } var hasStartedConnecting: Bool { get set } var hasEnded: Bool { get set } - func updateCallMessage(mode: EndCallMode) + func updateCallMessage(mode: EndCallMode, using dependencies: Dependencies) func didReceiveRemoteSDP(sdp: RTCSessionDescription) func startSessionCall(_ db: Database) } diff --git a/SessionMessagingKit/Calls/NoopSessionCallManager.swift b/SessionMessagingKit/Calls/NoopSessionCallManager.swift new file mode 100644 index 00000000000..afa3e928060 --- /dev/null +++ b/SessionMessagingKit/Calls/NoopSessionCallManager.swift @@ -0,0 +1,25 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import CallKit + +internal struct NoopSessionCallManager: CallManagerProtocol { + var currentCall: CurrentCallProtocol? + + func setCurrentCall(_ call: CurrentCallProtocol?) {} + func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) {} + func reportCurrentCallEnded(reason: CXCallEndedReason?) {} + func suspendDatabaseIfCallEndedInBackground() {} + + func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + + func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) {} + func handleICECandidates(message: CallMessage, sdpMLineIndexes: [UInt32], sdpMids: [String]) {} + func handleAnswerMessage(_ message: CallMessage) {} + + func currentWebRTCSessionMatches(callId: String) -> Bool { return false } + + func dismissAllCallUI() {} +} diff --git a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift index ae419ce5b44..336768cacd3 100644 --- a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift @@ -243,8 +243,14 @@ enum _013_SessionUtilChanges: Migration { /// counts so don't do this when running tests (this logic is the same as in `MainAppContext.isRunningTests` if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { if (try SessionThread.exists(db, id: userPublicKey)) == false { - try SessionThread - .fetchOrCreate(db, id: userPublicKey, variant: .contact, shouldBeVisible: false) + try SessionThread.upsert( + db, + id: userPublicKey, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + calledFromConfig: nil, + using: dependencies + ) } } diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift index 4f26d29cebb..99d04b493d6 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift @@ -197,7 +197,7 @@ enum _014_GenerateInitialUserConfigDumps: Migration { // MARK: - Threads - try LibSession.updatingThreads(db, Array(allThreads.values)) + try LibSession.updatingThreads(db, Array(allThreads.values), using: dependencies) // MARK: - Syncing diff --git a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift b/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift index 36d2d45800e..23853421850 100644 --- a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift +++ b/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift @@ -80,7 +80,7 @@ enum _018_DisappearingMessagesConfiguration: Migration { } // Update the configs so the settings are synced - _ = try LibSession.updatingDisappearingConfigs(db, contactUpdate) + _ = try LibSession.updatingDisappearingConfigs(db, contactUpdate, using: dependencies) _ = try LibSession.batchUpdate(db, disappearingConfigs: legacyGroupUpdate, using: dependencies) 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 7c1548f4187..6e426327435 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -593,7 +593,7 @@ extension Attachment { return UInt(floor(max(screenSizePoints.width, screenSizePoints.height) * minZoomFactor)) }() - private static var sharedDataAttachmentsDirPath: String = { + public static var sharedDataAttachmentsDirPath: String = { URL(fileURLWithPath: FileManager.default.appSharedDataDirectoryPath) .appendingPathComponent("Attachments") // stringlint:ignore .path diff --git a/SessionMessagingKit/Database/Models/BlindedIdLookup.swift b/SessionMessagingKit/Database/Models/BlindedIdLookup.swift index 90a6a89c69c..a5bfce2201f 100644 --- a/SessionMessagingKit/Database/Models/BlindedIdLookup.swift +++ b/SessionMessagingKit/Database/Models/BlindedIdLookup.swift @@ -137,7 +137,11 @@ public extension BlindedIdLookup { if isCheckingForOutbox && !contact.isApproved { try Contact .filter(id: contact.id) - .updateAllAndConfig(db, Contact.Columns.isApproved.set(to: true)) + .updateAllAndConfig( + db, + Contact.Columns.isApproved.set(to: true), + using: dependencies + ) } break diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 046d612a02e..20fe72b8be8 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -115,13 +115,15 @@ public extension ClosedGroup { _ db: Database? = nil, threadId: String, removeGroupData: Bool, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { try removeKeysAndUnsubscribe( db, threadIds: [threadId], removeGroupData: removeGroupData, - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) } @@ -129,16 +131,18 @@ public extension ClosedGroup { _ db: Database? = nil, threadIds: [String], removeGroupData: Bool, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { guard !threadIds.isEmpty else { return } guard let db: Database = db else { - Storage.shared.write { db in + dependencies.storage.write { db in try ClosedGroup.removeKeysAndUnsubscribe( db, threadIds: threadIds, removeGroupData: removeGroupData, - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) } return @@ -196,7 +200,8 @@ public extension ClosedGroup { db, legacyGroupIds: threadVariants .filter { $0.variant == .legacyGroup } - .map { $0.id } + .map { $0.id }, + using: dependencies ) try LibSession.remove( diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index e87ce2b5c7b..d9cd164de6f 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -491,7 +491,8 @@ public extension Interaction { threadId: String, threadVariant: SessionThread.Variant, includingOlder: Bool, - trySendReadReceipt: Bool + trySendReadReceipt: Bool, + using dependencies: Dependencies ) throws { guard let interactionId: Int64 = interactionId else { return } @@ -535,7 +536,8 @@ public extension Interaction { ], lastReadTimestampMs: timestampMs, trySendReadReceipt: trySendReadReceipt, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) return } @@ -560,7 +562,8 @@ public extension Interaction { interactionInfo: [interactionInfo], lastReadTimestampMs: interactionInfo.timestampMs, trySendReadReceipt: trySendReadReceipt, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) return } @@ -576,7 +579,8 @@ public extension Interaction { interactionInfo: interactionInfoToMarkAsRead, lastReadTimestampMs: interactionInfo.timestampMs, trySendReadReceipt: trySendReadReceipt, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -647,7 +651,8 @@ public extension Interaction { interactionInfo: [Interaction.ReadInfo], lastReadTimestampMs: Int64, trySendReadReceipt: Bool, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { guard !interactionInfo.isEmpty else { return } @@ -657,7 +662,8 @@ public extension Interaction { db, threadId: threadId, threadVariant: threadVariant, - lastReadTimestampMs: lastReadTimestampMs + lastReadTimestampMs: lastReadTimestampMs, + using: dependencies ) // Add the 'DisappearingMessagesJob' if needed - this will update any expiring @@ -799,32 +805,6 @@ public extension Interaction { return "\(threadId)-\(id)" } - func markingAsDeleted() -> Interaction { - return Interaction( - id: id, - serverHash: nil, - messageUuid: messageUuid, - threadId: threadId, - authorId: authorId, - variant: .standardIncomingDeleted, - body: nil, - timestampMs: timestampMs, - receivedAtTimestampMs: receivedAtTimestampMs, - wasRead: true, // Never consider deleted messages unread - hasMention: false, - expiresInSeconds: expiresInSeconds, - expiresStartedAtMs: expiresStartedAtMs, - linkPreviewUrl: nil, - openGroupServerMessageId: openGroupServerMessageId, - openGroupWhisper: openGroupWhisper, - openGroupWhisperMods: openGroupWhisperMods, - openGroupWhisperTo: openGroupWhisperTo, - state: .deleted, - recipientReadTimestampMs: nil, - mostRecentFailureText: nil - ) - } - static func isUserMentioned( _ db: Database, threadId: String, @@ -1146,3 +1126,56 @@ public extension Interaction.State { } } } + +// MARK: - Deletion + +public extension Interaction { + /// When deleting a message we should also delete any reactions which were on the message, so fetch and + /// return those hashes as well + static func serverHashesForDeletion( + _ db: Database, + interactionIds: Set, + additionalServerHashesToRemove: [String] = [] + ) throws -> Set { + let messageHashes: [String] = try Interaction + .filter(ids: interactionIds) + .filter(Interaction.Columns.serverHash != nil) + .select(.serverHash) + .asRequest(of: String.self) + .fetchAll(db) + let reactionHashes: [String] = try Reaction + .filter(interactionIds.contains(Reaction.Columns.interactionId)) + .filter(Reaction.Columns.serverHash != nil) + .select(.serverHash) + .asRequest(of: String.self) + .fetchAll(db) + + return Set(messageHashes + reactionHashes + additionalServerHashesToRemove) + } + + func markingAsDeleted() -> Interaction { + return Interaction( + id: id, + serverHash: nil, + messageUuid: messageUuid, + threadId: threadId, + authorId: authorId, + variant: .standardIncomingDeleted, + body: nil, + timestampMs: timestampMs, + receivedAtTimestampMs: receivedAtTimestampMs, + wasRead: true, // Never consider deleted messages unread + hasMention: false, + expiresInSeconds: expiresInSeconds, + expiresStartedAtMs: expiresStartedAtMs, + linkPreviewUrl: nil, + openGroupServerMessageId: openGroupServerMessageId, + openGroupWhisper: openGroupWhisper, + openGroupWhisperMods: openGroupWhisperMods, + openGroupWhisperTo: openGroupWhisperTo, + state: .deleted, + recipientReadTimestampMs: nil, + mostRecentFailureText: nil + ) + } +} diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 9dbe1153879..15f6d722448 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -148,52 +148,184 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, // MARK: - GRDB Interactions public extension SessionThread { - /// Fetches or creates a SessionThread with the specified id, variant and visible state + /// This type allows the specification of different `SessionThread` properties to use when creating/updating a thread, by default + /// it will attempt to use the values set in `libSession` if none are present + struct TargetValues { + public enum Value { + case setTo(T) + case useLibSession + + /// We should generally try to make `libSession` the source of truth for conversation settings (so they sync between + /// devices) but there are some cases where we don't want to modify a setting (eg. when handling a config change), so + /// this case can be used for those situations + case useExisting + + var valueOrNull: T? { + switch self { + case .setTo(let value): return value + default: return nil + } + } + } + + let creationDateTimestamp: TimeInterval + let shouldBeVisible: Value + let pinnedPriority: Value + let disappearingMessagesConfig: Value + + // MARK: - Convenience + + public static var existingOrDefault: TargetValues { + return TargetValues(shouldBeVisible: .useLibSession) + } + + // MARK: - Initialization + + public init( + creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), + shouldBeVisible: Value, + pinnedPriority: Value = .useLibSession, + disappearingMessagesConfig: Value = .useLibSession + ) { + self.creationDateTimestamp = creationDateTimestamp + self.shouldBeVisible = shouldBeVisible + self.pinnedPriority = pinnedPriority + self.disappearingMessagesConfig = disappearingMessagesConfig + } + } + + /// Updates or inserts a `SessionThread` with the specified `id`, `variant` and specified `values` /// - /// **Notes:** - /// - The `variant` will be ignored if an existing thread is found - /// - This method **will** save the newly created SessionThread to the database - @discardableResult static func fetchOrCreate( + /// **Note:** This method **will** save the newly created/updated `SessionThread` to the database + @discardableResult static func upsert( _ db: Database, id: ID, variant: Variant, - creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), - shouldBeVisible: Bool? + values: TargetValues, + calledFromConfig configTriggeringChange: LibSession.Config.Variant?, + using dependencies: Dependencies ) throws -> SessionThread { - guard let existingThread: SessionThread = try? fetchOne(db, id: id) else { - return try SessionThread( - id: id, - variant: variant, - creationDateTimestamp: creationDateTimestamp, - shouldBeVisible: (shouldBeVisible ?? false) - ).saved(db) + var result: SessionThread + + /// If the thread doesn't already exist then create it (with the provided defaults) + switch try? fetchOne(db, id: id) { + case .some(let existingThread): result = existingThread + case .none: + let targetPriority: Int32 = LibSession.pinnedPriority( + db, + threadId: id, + threadVariant: variant, + conf: configTriggeringChange?.conf, + using: dependencies + ) + + result = try SessionThread( + id: id, + variant: variant, + creationDateTimestamp: values.creationDateTimestamp, + shouldBeVisible: LibSession.shouldBeVisible(priority: targetPriority), + pinnedPriority: targetPriority + ).upserted(db) } - // If the `shouldBeVisible` state matches then we can finish early - guard - let desiredVisibility: Bool = shouldBeVisible, - existingThread.shouldBeVisible != desiredVisibility - else { return existingThread } + /// Setup the `DisappearingMessagesConfiguration` as specified + switch (variant, values.disappearingMessagesConfig) { + case (.community, _), (_, .useExisting): break // No need to do anything + case (_, .setTo(let config)): // Save the explicit config + try config + .upserted(db) + .clearUnrelatedControlMessages( + db, + threadVariant: variant + ) + + case (_, .useLibSession): // Create and save the config from libSession + guard configTriggeringChange == nil else { throw LibSessionError.invalidConfigAccess } + + try LibSession + .disappearingMessagesConfig( + db, + threadId: id, + threadVariant: variant, + conf: configTriggeringChange?.conf, + using: dependencies + )? + .upserted(db) + .clearUnrelatedControlMessages( + db, + threadVariant: variant + ) + } + + /// Apply any changes if the provided `values` don't match the current or default settings + var requiredChanges: [ConfigColumnAssignment] = [] + var finalShouldBeVisible: Bool = result.shouldBeVisible + var finalPinnedPriority: Int32? = result.pinnedPriority - // Update the `shouldBeVisible` state + /// The `shouldBeVisible` flag is based on `pinnedPriority` so we need to check these two together if they + /// should both be sourced from `libSession` + switch (values.pinnedPriority, values.shouldBeVisible) { + case (.useLibSession, .useLibSession): + let targetPriority: Int32 = LibSession.pinnedPriority( + db, + threadId: id, + threadVariant: variant, + conf: configTriggeringChange?.conf, + using: dependencies + ) + let libSessionShouldBeVisible: Bool = LibSession.shouldBeVisible(priority: targetPriority) + + if targetPriority != result.pinnedPriority { + requiredChanges.append(SessionThread.Columns.pinnedPriority.set(to: targetPriority)) + finalPinnedPriority = targetPriority + } + + if libSessionShouldBeVisible != result.shouldBeVisible { + requiredChanges.append(SessionThread.Columns.shouldBeVisible.set(to: libSessionShouldBeVisible)) + finalShouldBeVisible = libSessionShouldBeVisible + } + + default: break + } + + /// Otherwise we can just handle the explicit `setTo` cases for these + if case .setTo(let value) = values.pinnedPriority, value != result.pinnedPriority { + requiredChanges.append(SessionThread.Columns.pinnedPriority.set(to: value)) + finalPinnedPriority = value + } + + if case .setTo(let value) = values.shouldBeVisible, value != result.shouldBeVisible { + requiredChanges.append(SessionThread.Columns.shouldBeVisible.set(to: value)) + finalShouldBeVisible = value + } + + /// If no changes were needed we can just return the existing/default thread + guard !requiredChanges.isEmpty else { return result } + + /// Otherwise save the changes try SessionThread .filter(id: id) .updateAllAndConfig( db, - SessionThread.Columns.shouldBeVisible.set(to: shouldBeVisible) + requiredChanges, + calledFromConfig: (configTriggeringChange != nil), + using: dependencies ) - // Retrieve the updated thread and return it (we don't recursively call this method - // just in case something weird happened and the above update didn't work, as that - // would result in an infinite loop) + /// We need to re-fetch the updated thread as the changes wouldn't have been applied to `result`, it's also possible additional + /// changes could have happened to the thread during the database operations + /// + /// Since we want to avoid returning a nullable `SessionThread` here we need to fallback to a non-null instance, but it should + /// never be called return (try fetchOne(db, id: id)) .defaulting( to: try SessionThread( id: id, variant: variant, - creationDateTimestamp: creationDateTimestamp, - shouldBeVisible: desiredVisibility - ).saved(db) + creationDateTimestamp: values.creationDateTimestamp, + shouldBeVisible: finalShouldBeVisible, + pinnedPriority: finalPinnedPriority + ).upserted(db) ) } @@ -282,7 +414,9 @@ public extension SessionThread { public extension SessionThread { enum DeletionType { - case hideContactConversationAndDeleteContent + case hideContactConversation + case hideContactConversationAndDeleteContentDirectly + case deleteContactConversationAndMarkHidden case deleteContactConversationAndContact case leaveGroupAsync case deleteGroupAndContent @@ -293,13 +427,15 @@ public extension SessionThread { _ db: Database, type: SessionThread.DeletionType, threadId: String, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { try deleteOrLeave( db, type: type, threadIds: [threadId], - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) } @@ -307,46 +443,74 @@ public extension SessionThread { _ db: Database, type: SessionThread.DeletionType, threadIds: [String], - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let remainingThreadIds: Set = threadIds.asSet().removing(currentUserPublicKey) switch type { - case .hideContactConversationAndDeleteContent: + case .hideContactConversation: + _ = try SessionThread + .filter(ids: threadIds) + .updateAllAndConfig( + db, + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), + SessionThread.Columns.shouldBeVisible.set(to: false), + calledFromConfig: calledFromConfigHandling, + using: dependencies + ) + + case .hideContactConversationAndDeleteContentDirectly: // Clear any interactions for the deleted thread _ = try Interaction .filter(threadIds.contains(Interaction.Columns.threadId)) .deleteAll(db) + // Hide the threads + _ = try SessionThread + .filter(ids: threadIds) + .updateAllAndConfig( + db, + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), + SessionThread.Columns.shouldBeVisible.set(to: false), + calledFromConfig: calledFromConfigHandling, + using: dependencies + ) + + case .deleteContactConversationAndMarkHidden: + _ = try SessionThread + .filter(ids: remainingThreadIds) + .deleteAll(db) + // We need to custom handle the 'Note to Self' conversation (it should just be // hidden locally rather than deleted) if threadIds.contains(currentUserPublicKey) { + // Clear any interactions for the deleted thread + _ = try Interaction + .filter(Interaction.Columns.threadId == currentUserPublicKey) + .deleteAll(db) + _ = try SessionThread .filter(id: currentUserPublicKey) .updateAllAndConfig( db, + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), + SessionThread.Columns.shouldBeVisible.set(to: false), calledFromConfig: calledFromConfigHandling, - SessionThread.Columns.pinnedPriority.set(to: 0), - SessionThread.Columns.shouldBeVisible.set(to: false) + using: dependencies ) } - // Update any other threads to be hidden (don't want to actually delete the thread - // record in case it's settings get changed while it's not visible) - _ = try SessionThread - .filter(ids: remainingThreadIds) - .updateAllAndConfig( - db, - calledFromConfig: calledFromConfigHandling, - SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false) - ) + if !calledFromConfigHandling { + // Update any other threads to be hidden + try LibSession.hide(db, contactIds: Array(remainingThreadIds), using: dependencies) + } case .deleteContactConversationAndContact: // If this wasn't called from config handling then we need to hide the conversation if !calledFromConfigHandling { - try LibSession.remove(db, contactIds: Array(remainingThreadIds)) + try LibSession.remove(db, contactIds: Array(remainingThreadIds), using: dependencies) } _ = try SessionThread @@ -367,7 +531,8 @@ public extension SessionThread { db, threadIds: threadIds, removeGroupData: true, - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) case .deleteCommunityAndContent: diff --git a/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift index c771eb5ff4a..e3976ea959f 100644 --- a/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift @@ -127,7 +127,8 @@ public enum GroupLeavingJob: JobExecutor { db, threadId: threadId, removeGroupData: details.deleteThread, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 331931be3f7..60c3f22b2d2 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -35,10 +35,11 @@ internal extension LibSession { _ db: Database, in conf: UnsafeMutablePointer?, mergeNeedsDump: Bool, - latestConfigSentTimestampMs: Int64 + latestConfigSentTimestampMs: Int64, + using dependencies: Dependencies ) throws { guard mergeNeedsDump else { return } - guard conf != nil else { throw LibSessionError.nilConfigObject } + guard let conf: UnsafeMutablePointer = conf else { throw LibSessionError.nilConfigObject } // The current users contact data is handled separately so exclude it if it's present (as that's // actually a bug) @@ -140,52 +141,49 @@ internal extension LibSession { .fetchOne(db) let threadExists: Bool = (threadInfo != nil) let updatedShouldBeVisible: Bool = LibSession.shouldBeVisible(priority: data.priority) - - /// If we are hiding the conversation then kick the user from it if it's currently open - if !updatedShouldBeVisible { - LibSession.kickFromConversationUIIfNeeded(removedThreadIds: [sessionId]) - } - /// Create the thread if it doesn't exist, otherwise just update it's state - if !threadExists { - try SessionThread( - id: sessionId, - variant: .contact, - creationDateTimestamp: data.created, - shouldBeVisible: updatedShouldBeVisible, - pinnedPriority: data.priority - ).save(db) - } - else { - let changes: [ConfigColumnAssignment] = [ - (threadInfo?.shouldBeVisible == updatedShouldBeVisible ? nil : - SessionThread.Columns.shouldBeVisible.set(to: updatedShouldBeVisible) - ), - (threadInfo?.pinnedPriority == data.priority ? nil : - SessionThread.Columns.pinnedPriority.set(to: data.priority) + switch (updatedShouldBeVisible, threadExists) { + /// If we are hiding the conversation then kick the user from it if it's currently open then delete the thread + case (false, true): + LibSession.kickFromConversationUIIfNeeded(removedThreadIds: [sessionId]) + + try SessionThread.deleteOrLeave( + db, + type: .deleteContactConversationAndMarkHidden, + threadId: sessionId, + calledFromConfigHandling: true, + using: dependencies ) - ].compactMap { $0 } - try SessionThread - .filter(id: sessionId) - .updateAll( // Handling a config update so don't use `updateAllAndConfig` - db, - changes + /// We need to create or update the thread + case (true, _): + let localConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration + .fetchOne(db, id: sessionId) + .defaulting(to: DisappearingMessagesConfiguration.defaultWith(sessionId)) + let disappearingMessagesConfigChanged: Bool = ( + data.config.isValidV2Config() && + data.config != localConfig ) - } - - // Update disappearing messages configuration if needed - let localConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration - .fetchOne(db, id: sessionId) - .defaulting(to: DisappearingMessagesConfiguration.defaultWith(sessionId)) - - if data.config.isValidV2Config() && data.config != localConfig { - try data.config - .saved(db) - .clearUnrelatedControlMessages( + + _ = try SessionThread.upsert( db, - threadVariant: .contact + id: sessionId, + variant: .contact, + values: SessionThread.TargetValues( + creationDateTimestamp: data.created, + shouldBeVisible: .setTo(updatedShouldBeVisible), + pinnedPriority: .setTo(data.priority), + disappearingMessagesConfig: (disappearingMessagesConfigChanged ? + .setTo(data.config) : + .useExisting + ) + ), + calledFromConfig: .contacts(conf), + using: dependencies ) + + /// Thread shouldn't be visible and doesn't exist so no need to do anything + case (false, false): break } } @@ -203,8 +201,8 @@ internal extension LibSession { .filter(SessionThread.Columns.variant == SessionThread.Variant.contact) .filter( /// Only want to include include standard contact conversations (not blinded conversations) - ClosedGroup.Columns.threadId > SessionId.Prefix.standard.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.standard.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.standard.rawValue && + SessionThread.Columns.id < SessionId.Prefix.standard.endOfRangeString ) .select(.id) .asRequest(of: String.self) @@ -253,10 +251,15 @@ internal extension LibSession { db, type: .deleteContactConversationAndContact, threadIds: combinedIds, - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) - try LibSession.remove(db, volatileContactIds: combinedIds) + try LibSession.remove( + db, + volatileContactIds: combinedIds, + using: dependencies + ) } } @@ -368,7 +371,11 @@ internal extension LibSession { // MARK: - Outgoing Changes internal extension LibSession { - static func updatingContacts(_ db: Database, _ updated: [T]) throws -> [T] { + static func updatingContacts( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedContacts: [Contact] = updated as? [Contact] else { throw StorageError.generic } // The current users contact data doesn't need to sync so exclude it, we also don't want to sync @@ -386,7 +393,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in // When inserting new contacts (or contacts with invalid profile data) we want // to add any valid profile information we have so identify if any of the updated @@ -428,7 +436,11 @@ internal extension LibSession { return updated } - static func updatingProfiles(_ db: Database, _ updated: [T]) throws -> [T] { + static func updatingProfiles( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedProfiles: [Profile] = updated as? [Profile] else { throw StorageError.generic } // We should only sync profiles which are associated to contact data to avoid including profiles @@ -459,7 +471,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.update( profile: updatedUserProfile, @@ -471,7 +484,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession .upsert( @@ -484,7 +498,11 @@ internal extension LibSession { return updated } - static func updatingDisappearingConfigs(_ db: Database, _ updated: [T]) throws -> [T] { + static func updatingDisappearingConfigs( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedDisappearingConfigs: [DisappearingMessagesConfiguration] = updated as? [DisappearingMessagesConfiguration] else { throw StorageError.generic } // We should only sync disappearing messages configs which are associated to existing contacts @@ -513,7 +531,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateNoteToSelf( disappearingMessagesConfig: updatedUserDisappearingConfig, @@ -525,7 +544,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession .upsert( @@ -542,11 +562,16 @@ internal extension LibSession { // MARK: - External Outgoing Changes public extension LibSession { - static func hide(_ db: Database, contactIds: [String]) throws { + static func hide( + _ db: Database, + contactIds: [String], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in // Mark the contacts as hidden try LibSession.upsert( @@ -562,13 +587,18 @@ public extension LibSession { } } - static func remove(_ db: Database, contactIds: [String]) throws { + static func remove( + _ db: Database, + contactIds: [String], + using dependencies: Dependencies + ) throws { guard !contactIds.isEmpty else { return } try LibSession.performAndPushChange( db, for: .contacts, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in contactIds.forEach { sessionId in guard var cSessionId: [CChar] = sessionId.cString(using: .utf8) else { return } @@ -582,7 +612,8 @@ public extension LibSession { static func update( _ db: Database, sessionId: String, - disappearingMessagesConfig: DisappearingMessagesConfiguration + disappearingMessagesConfig: DisappearingMessagesConfiguration, + using dependencies: Dependencies ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -591,7 +622,8 @@ public extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateNoteToSelf( disappearingMessagesConfig: disappearingMessagesConfig, @@ -603,7 +635,8 @@ public extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession .upsert( diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift index 65ec663bcfa..aecd92557f8 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift @@ -17,7 +17,8 @@ internal extension LibSession { static func handleConvoInfoVolatileUpdate( _ db: Database, in conf: UnsafeMutablePointer?, - mergeNeedsDump: Bool + mergeNeedsDump: Bool, + using dependencies: Dependencies ) throws { guard mergeNeedsDump else { return } guard conf != nil else { throw LibSessionError.nilConfigObject } @@ -97,7 +98,8 @@ internal extension LibSession { interactionInfo: interactionInfoToMarkAsRead, lastReadTimestampMs: lastReadTimestampMs, trySendReadReceipt: false, // Interactions already read, no need to send - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) return nil } @@ -226,7 +228,8 @@ internal extension LibSession { static func updateMarkedAsUnreadState( _ db: Database, - threads: [SessionThread] + threads: [SessionThread], + using dependencies: Dependencies ) throws { // If we have no updated threads then no need to continue guard !threads.isEmpty else { return } @@ -245,7 +248,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db), + using: dependencies ) { conf in try upsert( convoInfoVolatileChanges: changes, @@ -254,11 +258,16 @@ internal extension LibSession { } } - static func remove(_ db: Database, volatileContactIds: [String]) throws { + static func remove( + _ db: Database, + volatileContactIds: [String], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try volatileContactIds.forEach { contactId in var cSessionId: [CChar] = try contactId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -269,11 +278,16 @@ internal extension LibSession { } } - static func remove(_ db: Database, volatileLegacyGroupIds: [String]) throws { + static func remove( + _ db: Database, + volatileLegacyGroupIds: [String], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try volatileLegacyGroupIds.forEach { legacyGroupId in var cLegacyGroupId: [CChar] = try legacyGroupId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -284,11 +298,16 @@ internal extension LibSession { } } - static func remove(_ db: Database, volatileCommunityInfo: [OpenGroupUrlInfo]) throws { + static func remove( + _ db: Database, + volatileCommunityInfo: [OpenGroupUrlInfo], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try volatileCommunityInfo.forEach { urlInfo in var cBaseUrl: [CChar] = try urlInfo.server.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -308,12 +327,14 @@ public extension LibSession { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - lastReadTimestampMs: Int64 + lastReadTimestampMs: Int64, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try upsert( convoInfoVolatileChanges: [ diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 5d06cc6eac1..fb84e412ea9 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -13,6 +13,10 @@ public extension LibSession { enum Crypto { public typealias Domain = String } + + /// The default priority for newly created threads - the default value is for threads to be hidden as we explicitly make threads visible + /// when sending or receiving messages + static var defaultNewThreadPriority: Int32 { return hiddenPriority } /// A `0` `priority` value indicates visible, but not pinned static let visiblePriority: Int32 = 0 @@ -54,7 +58,7 @@ internal extension LibSession { _ db: Database, for variant: ConfigDump.Variant, publicKey: String, - using dependencies: Dependencies = Dependencies(), + using dependencies: Dependencies, change: (UnsafeMutablePointer?) throws -> () ) throws { // Since we are doing direct memory manipulation we are using an `Atomic` @@ -96,7 +100,11 @@ internal extension LibSession { } } - @discardableResult static func updatingThreads(_ db: Database, _ updated: [T]) throws -> [T] { + @discardableResult static func updatingThreads( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedThreads: [SessionThread] = updated as? [SessionThread] else { throw StorageError.generic } @@ -112,7 +120,7 @@ internal extension LibSession { .reduce(into: [:]) { result, next in result[next.threadId] = next } // Update the unread state for the threads first (just in case that's what changed) - try LibSession.updateMarkedAsUnreadState(db, threads: updatedThreads) + try LibSession.updateMarkedAsUnreadState(db, threads: updatedThreads, using: dependencies) // Then update the `hidden` and `priority` values try groupedThreads.forEach { variant, threads in @@ -124,7 +132,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateNoteToSelf( priority: { @@ -147,7 +156,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.upsert( contactData: remainingThreads @@ -171,7 +181,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.upsert( communities: threads @@ -193,7 +204,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.upsert( legacyGroups: threads @@ -237,7 +249,11 @@ internal extension LibSession { } } - static func updatingSetting(_ db: Database, _ updated: Setting?) throws { + static func updatingSetting( + _ db: Database, + _ updated: Setting?, + using dependencies: Dependencies + ) throws { // Don't current support any nullable settings guard let updatedSetting: Setting = updated else { return } @@ -249,7 +265,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateSettings( checkForCommunityMessageRequests: updatedSetting.unsafeValue(as: Bool.self), @@ -389,6 +406,180 @@ internal extension LibSession { // MARK: - External Outgoing Changes public extension LibSession { + static func pinnedPriority( + _ db: Database, + threadId: String, + threadVariant: SessionThread.Variant, + conf: UnsafeMutablePointer?, + using dependencies: Dependencies + ) -> Int32 { + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let configVariant: ConfigDump.Variant = { + switch threadVariant { + case .contact: return (threadId == userPublicKey ? .userProfile : .contacts) + case .legacyGroup, .group, .community: return .userGroups + } + }() + + guard let conf: UnsafeMutablePointer = conf else { + return dependencies.caches[.libSession] + .config(for: configVariant, publicKey: userPublicKey) + .wrappedValue + .map { conf in + LibSession.pinnedPriority( + db, + threadId: threadId, + threadVariant: threadVariant, + conf: conf, + using: dependencies + ) + } + .defaulting(to: LibSession.defaultNewThreadPriority) + } + + guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { + return LibSession.defaultNewThreadPriority + } + + switch threadVariant { + case .contact where threadId == userPublicKey: + return user_profile_get_nts_priority(conf) + + case .contact: + var contact: contacts_contact = contacts_contact() + + guard contacts_get(conf, &contact, &cThreadId) else { + LibSessionError.clear(conf) + return LibSession.defaultNewThreadPriority + } + + return contact.priority + + case .community: + let maybeUrlInfo: OpenGroupUrlInfo? = Storage.shared + .read { db in try OpenGroupUrlInfo.fetchAll(db, ids: [threadId]) }? + .first + + guard + let urlInfo: OpenGroupUrlInfo = maybeUrlInfo, + var cBaseUrl: [CChar] = urlInfo.server.cString(using: .utf8), + var cRoom: [CChar] = urlInfo.roomToken.cString(using: .utf8) + else { return LibSession.defaultNewThreadPriority } + + var community: ugroups_community_info = ugroups_community_info() + let result: Bool = user_groups_get_community(conf, &community, &cBaseUrl, &cRoom) + LibSessionError.clear(conf) + + return community.priority + + case .legacyGroup: + let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) + LibSessionError.clear(conf) + + defer { + if groupInfo != nil { + ugroups_legacy_group_free(groupInfo) + } + } + + return (groupInfo?.pointee.priority ?? LibSession.defaultNewThreadPriority) + + case .group: + return LibSession.defaultNewThreadPriority // FIXME: Add in groups rebuild + } + } + + static func disappearingMessagesConfig( + _ db: Database, + threadId: String, + threadVariant: SessionThread.Variant, + conf: UnsafeMutablePointer?, + using dependencies: Dependencies + ) -> DisappearingMessagesConfiguration? { + switch threadVariant { + case .community: return nil + default: break + } + + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let configVariant: ConfigDump.Variant = { + switch threadVariant { + case .contact: return (threadId == userPublicKey ? .userProfile : .contacts) + case .legacyGroup, .group, .community: return .userGroups + } + }() + + guard let conf: UnsafeMutablePointer = conf else { + return dependencies.caches[.libSession] + .config(for: configVariant, publicKey: userPublicKey) + .wrappedValue + .map { conf in + LibSession.disappearingMessagesConfig( + db, + threadId: threadId, + threadVariant: threadVariant, + conf: conf, + using: dependencies + ) + } + } + + guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { return nil } + + switch threadVariant { + case .community: return nil + case .contact where threadId == userPublicKey: + let targetExpiry: Int32 = user_profile_get_nts_expiry(conf) + let targetIsEnabled: Bool = (targetExpiry > 0) + + return DisappearingMessagesConfiguration( + threadId: threadId, + isEnabled: targetIsEnabled, + durationSeconds: TimeInterval(targetExpiry), + type: targetIsEnabled ? .disappearAfterSend : .unknown + ) + + case .contact: + var contact: contacts_contact = contacts_contact() + + guard contacts_get(conf, &contact, &cThreadId) else { + LibSessionError.clear(conf) + return nil + } + + return DisappearingMessagesConfiguration( + threadId: threadId, + isEnabled: contact.exp_seconds > 0, + durationSeconds: TimeInterval(contact.exp_seconds), + type: DisappearingMessagesConfiguration.DisappearingMessageType( + libSessionType: contact.exp_mode + ) + ) + + case .legacyGroup: + let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) + LibSessionError.clear(conf) + + defer { + if groupInfo != nil { + ugroups_legacy_group_free(groupInfo) + } + } + + return groupInfo.map { info in + DisappearingMessagesConfiguration( + threadId: userPublicKey, + isEnabled: (info.pointee.disappearing_timer > 0), + durationSeconds: TimeInterval(info.pointee.disappearing_timer), + type: .disappearAfterSend + ) + } + + case .group: + return nil // FIXME: Add in groups rebuild + } + } + static func conversationInConfig( _ db: Database? = nil, threadId: String, @@ -396,8 +587,8 @@ public extension LibSession { visibleOnly: Bool, using dependencies: Dependencies ) -> Bool { - // Currently blinded conversations cannot be contained in the config, so there is no point checking (it'll always be - // false) + // Currently blinded conversations cannot be contained in the config, so there is no + // point checking (it'll always be false) guard threadVariant == .community || ( (try? SessionId(from: threadId))?.prefix != .blinded15 && diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index d489f22de98..af38e10fab6 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -32,7 +32,7 @@ internal extension LibSession { using dependencies: Dependencies = Dependencies() ) throws { guard mergeNeedsDump else { return } - guard conf != nil else { throw LibSessionError.nilConfigObject } + guard let conf: UnsafeMutablePointer = conf else { throw LibSessionError.nilConfigObject } var infiniteLoopGuard: Int = 0 var communities: [PrioritisedData] = [] @@ -133,14 +133,13 @@ internal extension LibSession { // Add any new communities (via the OpenGroupManager) communities.forEach { community in - let successfullyAddedGroup: Bool = OpenGroupManager.shared - .add( - db, - roomToken: community.data.roomToken, - server: community.data.server, - publicKey: community.data.publicKey, - calledFromConfigHandling: true - ) + let successfullyAddedGroup: Bool = OpenGroupManager.shared.add( + db, + roomToken: community.data.roomToken, + server: community.data.server, + publicKey: community.data.publicKey, + calledFromConfig: .userGroups(conf) + ) if successfullyAddedGroup { db.afterNextTransactionNested { _ in @@ -181,7 +180,8 @@ internal extension LibSession { db, type: .deleteCommunityAndContent, threadIds: Array(communityIdsToRemove), - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) } @@ -240,7 +240,7 @@ internal extension LibSession { admins: updatedAdmins.map { $0.profileId }, expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0), formationTimestampMs: UInt64(joinedAt * 1000), - calledFromConfigHandling: true, + calledFromConfig: .userGroups(conf), using: dependencies ) } @@ -365,13 +365,13 @@ internal extension LibSession { if !legacyGroupIdsToRemove.isEmpty { LibSession.kickFromConversationUIIfNeeded(removedThreadIds: Array(legacyGroupIdsToRemove)) - try SessionThread - .deleteOrLeave( - db, - type: .deleteGroupAndContent, - threadIds: Array(legacyGroupIdsToRemove), - calledFromConfigHandling: true - ) + try SessionThread.deleteOrLeave( + db, + type: .deleteGroupAndContent, + threadIds: Array(legacyGroupIdsToRemove), + calledFromConfigHandling: true, + using: dependencies + ) } // MARK: -- Handle Group Changes @@ -546,12 +546,14 @@ public extension LibSession { _ db: Database, server: String, rootToken: String, - publicKey: String + publicKey: String, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try LibSession.upsert( communities: [ @@ -569,11 +571,17 @@ public extension LibSession { } } - static func remove(_ db: Database, server: String, roomToken: String) throws { + static func remove( + _ db: Database, + server: String, + roomToken: String, + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in var cBaseUrl: [CChar] = try server.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() var cRoom: [CChar] = try roomToken.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -592,7 +600,8 @@ public extension LibSession { roomToken: roomToken, publicKey: "" ) - ] + ], + using: dependencies ) } @@ -608,12 +617,14 @@ public extension LibSession { latestKeyPairReceivedTimestamp: TimeInterval, disappearingConfig: DisappearingMessagesConfiguration, members: Set, - admins: Set + admins: Set, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in guard conf != nil else { throw LibSessionError.nilConfigObject } @@ -674,12 +685,14 @@ public extension LibSession { latestKeyPair: ClosedGroupKeyPair? = nil, disappearingConfig: DisappearingMessagesConfiguration? = nil, members: Set? = nil, - admins: Set? = nil + admins: Set? = nil, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try LibSession.upsert( legacyGroups: [ @@ -736,13 +749,18 @@ public extension LibSession { } } - static func remove(_ db: Database, legacyGroupIds: [String]) throws { + static func remove( + _ db: Database, + legacyGroupIds: [String], + using dependencies: Dependencies + ) throws { guard !legacyGroupIds.isEmpty else { return } try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in legacyGroupIds.forEach { threadId in guard var cGroupId: [CChar] = threadId.cString(using: .utf8) else { return } @@ -753,7 +771,7 @@ public extension LibSession { } // Remove the volatile info as well - try LibSession.remove(db, volatileLegacyGroupIds: legacyGroupIds) + try LibSession.remove(db, volatileLegacyGroupIds: legacyGroupIds, using: dependencies) } // MARK: -- Group Changes diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 44b1f7d239d..ac3b04ac50b 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -28,7 +28,7 @@ internal extension LibSession { typealias ProfileData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?) guard mergeNeedsDump else { return } - guard conf != nil else { throw LibSessionError.nilConfigObject } + guard let conf: UnsafeMutablePointer = conf else { throw LibSessionError.nilConfigObject } // A profile must have a name so if this is null then it's invalid and can be ignored guard let profileNamePtr: UnsafePointer = user_profile_get_name(conf) else { return } @@ -86,30 +86,28 @@ internal extension LibSession { } } else { - try SessionThread - .fetchOrCreate( - db, - id: userPublicKey, - variant: .contact, - shouldBeVisible: LibSession.shouldBeVisible(priority: targetPriority) - ) - - try SessionThread - .filter(id: userPublicKey) - .updateAll( // Handling a config update so don't use `updateAllAndConfig` - db, - SessionThread.Columns.pinnedPriority.set(to: targetPriority) - ) - // If the 'Note to Self' conversation is hidden then we should trigger the proper - // `deleteOrLeave` behaviour (for 'Note to Self' this will leave the conversation - // but remove the associated interactions) + // `deleteOrLeave` behaviour if !LibSession.shouldBeVisible(priority: targetPriority) { try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .hideContactConversation, threadId: userPublicKey, - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies + ) + } + else { + try SessionThread.upsert( + db, + id: userPublicKey, + variant: .contact, + values: SessionThread.TargetValues( + shouldBeVisible: .setTo(LibSession.shouldBeVisible(priority: targetPriority)), + pinnedPriority: .setTo(targetPriority) + ), + calledFromConfig: .userProfile(conf), + using: dependencies ) } } @@ -213,6 +211,30 @@ internal extension LibSession { } } +// MARK: - External Outgoing Changes + +public extension LibSession { + static func updateNoteToSelf( + _ db: Database, + priority: Int32? = nil, + disappearingMessagesConfig: DisappearingMessagesConfiguration? = nil, + using dependencies: Dependencies + ) throws { + try LibSession.performAndPushChange( + db, + for: .userProfile, + publicKey: getUserHexEncodedPublicKey(db), + using: dependencies + ) { conf in + try LibSession.updateNoteToSelf( + priority: priority, + disappearingMessagesConfig: disappearingMessagesConfig, + in: conf + ) + } + } +} + // MARK: - Direct Values extension LibSession { diff --git a/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift b/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift index eafcd4898c6..772bd3f2a55 100644 --- a/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift +++ b/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift @@ -52,17 +52,24 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table @discardableResult func updateAllAndConfig( _ db: Database, + _ assignments: ConfigColumnAssignment..., calledFromConfig: Bool = false, - _ assignments: ConfigColumnAssignment... + using dependencies: Dependencies ) throws -> Int { - return try updateAllAndConfig(db, calledFromConfig: calledFromConfig, assignments) + return try updateAllAndConfig( + db, + assignments, + calledFromConfig: calledFromConfig, + using: dependencies + ) } @discardableResult func updateAllAndConfig( _ db: Database, + _ assignments: [ConfigColumnAssignment], calledFromConfig: Bool = false, - _ assignments: [ConfigColumnAssignment] + using dependencies: Dependencies ) throws -> Int { let targetAssignments: [ColumnAssignment] = assignments.map { $0.assignment } @@ -71,7 +78,12 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table return try self.updateAll(db, targetAssignments) } - return try self.updateAndFetchAllAndUpdateConfig(db, calledFromConfig: calledFromConfig, assignments).count + return try self.updateAndFetchAllAndUpdateConfig( + db, + assignments, + calledFromConfig: calledFromConfig, + using: dependencies + ).count } // MARK: -- updateAndFetchAll @@ -79,17 +91,24 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table @discardableResult func updateAndFetchAllAndUpdateConfig( _ db: Database, + _ assignments: ConfigColumnAssignment..., calledFromConfig: Bool = false, - _ assignments: ConfigColumnAssignment... + using dependencies: Dependencies ) throws -> [RowDecoder] { - return try updateAndFetchAllAndUpdateConfig(db, calledFromConfig: calledFromConfig, assignments) + return try updateAndFetchAllAndUpdateConfig( + db, + assignments, + calledFromConfig: calledFromConfig, + using: dependencies + ) } @discardableResult func updateAndFetchAllAndUpdateConfig( _ db: Database, + _ assignments: [ConfigColumnAssignment], calledFromConfig: Bool = false, - _ assignments: [ConfigColumnAssignment] + using dependencies: Dependencies ) throws -> [RowDecoder] { // First perform the actual updates let updatedData: [RowDecoder] = try self.updateAndFetchAll(db, assignments.map { $0.assignment }) @@ -97,7 +116,7 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table // Then check if any of the changes could affect the config guard !calledFromConfig && - LibSession.assignmentsRequireConfigUpdate(assignments) + LibSession.assignmentsRequireConfigUpdate(assignments) else { return updatedData } defer { @@ -114,13 +133,13 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table // Update the config dump state where needed switch self { case is QueryInterfaceRequest: - return try LibSession.updatingContacts(db, updatedData) + return try LibSession.updatingContacts(db, updatedData, using: dependencies) case is QueryInterfaceRequest: - return try LibSession.updatingProfiles(db, updatedData) + return try LibSession.updatingProfiles(db, updatedData, using: dependencies) case is QueryInterfaceRequest: - return try LibSession.updatingThreads(db, updatedData) + return try LibSession.updatingThreads(db, updatedData, using: dependencies) default: return updatedData } diff --git a/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift b/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift index eeaf760cf9d..efcb9f2feff 100644 --- a/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift +++ b/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift @@ -5,39 +5,103 @@ 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.BoolKey, + to newValue: Bool, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - 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.DoubleKey, + to newValue: Double?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - 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.IntKey, + to newValue: Int?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - 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.StringKey, + to newValue: String?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - 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?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - 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?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } /// 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)) + func setAndUpdateConfig( + _ key: Setting.DateKey, + to newValue: Date?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } private func updateConfigIfNeeded( _ db: Database, key: String, - updatedSetting: Setting? + updatedSetting: Setting?, + using dependencies: Dependencies ) throws { // Before we do anything custom make sure the setting should trigger a change guard LibSession.syncedSettings.contains(key) else { return } @@ -53,6 +117,6 @@ public extension Database { } } - try LibSession.updatingSetting(db, updatedSetting) + try LibSession.updatingSetting(db, updatedSetting, using: dependencies) } } diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 16a2bad9e4f..7fe2c731ca4 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -79,7 +79,7 @@ public extension LibSession { // If we weren't given a database instance then get one guard let db: Database = db else { - Storage.shared.read { db in + dependencies.storage.read { db in LibSession.loadState(db, userPublicKey: userPublicKey, ed25519SecretKey: secretKey, using: dependencies) } return @@ -457,14 +457,16 @@ public extension LibSession { db, in: conf, mergeNeedsDump: config_needs_dump(conf), - latestConfigSentTimestampMs: latestConfigSentTimestampMs + latestConfigSentTimestampMs: latestConfigSentTimestampMs, + using: dependencies ) case .convoInfoVolatile: try LibSession.handleConvoInfoVolatileUpdate( db, in: conf, - mergeNeedsDump: config_needs_dump(conf) + mergeNeedsDump: config_needs_dump(conf), + using: dependencies ) case .userGroups: diff --git a/SessionMessagingKit/LibSession/Types/Config.swift b/SessionMessagingKit/LibSession/Types/Config.swift new file mode 100644 index 00000000000..bf0c57ec334 --- /dev/null +++ b/SessionMessagingKit/LibSession/Types/Config.swift @@ -0,0 +1,28 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +public extension LibSession { + // MARK: - Config + + enum Config { + public enum Variant { + case userProfile(UnsafeMutablePointer) + case contacts(UnsafeMutablePointer) + case convoInfoVolatile(UnsafeMutablePointer) + case userGroups(UnsafeMutablePointer) + + var conf: UnsafeMutablePointer { + switch self { + case .userProfile(let value), .contacts(let value), + .convoInfoVolatile(let value), .userGroups(let value): + return value + } + } + } + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 0b2aadae4d4..b4969484b2c 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -180,12 +180,12 @@ public final class OpenGroupManager { roomToken: String, server: String, publicKey: String, - calledFromConfigHandling: Bool, + calledFromConfig configTriggeringChange: LibSession.Config.Variant?, using dependencies: Dependencies = Dependencies() ) -> Bool { // If we are currently polling for this server and already have a TSGroupThread for this room the do nothing if hasExistingOpenGroup(db, roomToken: roomToken, server: server, publicKey: publicKey, using: dependencies) { - SNLog("Ignoring join open group attempt (already joined), user initiated: \(!calledFromConfigHandling)") + SNLog("Ignoring join open group attempt (already joined), user initiated: \(configTriggeringChange != nil)") return false } @@ -201,18 +201,21 @@ public final class OpenGroupManager { // Optionally try to insert a new version of the OpenGroup (it will fail if there is already an // inactive one but that won't matter as we then activate it) - _ = try? SessionThread - .fetchOrCreate( - db, - id: threadId, - variant: .community, + _ = try? SessionThread.upsert( + db, + id: threadId, + variant: .community, + values: SessionThread.TargetValues( /// If we didn't add this open group via config handling then flag it to be visible (if it did come via config handling then /// we want to wait until it actually has messages before making it visible) /// /// **Note:** We **MUST** provide a `nil` value if this method was called from the config handling as updating /// the `shouldVeVisible` state can trigger a config update which could result in an infinite loop in the future - shouldBeVisible: (calledFromConfigHandling ? nil : true) - ) + shouldBeVisible: (configTriggeringChange != nil ? .useExisting : .setTo(true)) + ), + calledFromConfig: configTriggeringChange, + using: dependencies + ) if (try? OpenGroup.exists(db, id: threadId)) == false { try? OpenGroup @@ -222,7 +225,7 @@ public final class OpenGroupManager { // Set the group to active and reset the sequenceNumber (handle groups which have // been deactivated) - if calledFromConfigHandling { + if configTriggeringChange != nil { _ = try? OpenGroup .filter(id: OpenGroup.idFor(roomToken: roomToken, server: targetServer)) .updateAll( // Handling a config update so don't use `updateAllAndConfig` @@ -237,7 +240,8 @@ public final class OpenGroupManager { .updateAllAndConfig( db, OpenGroup.Columns.isActive.set(to: true), - OpenGroup.Columns.sequenceNumber.set(to: 0) + OpenGroup.Columns.sequenceNumber.set(to: 0), + using: dependencies ) } @@ -287,7 +291,8 @@ public final class OpenGroupManager { db, server: server, rootToken: roomToken, - publicKey: publicKey + publicKey: publicKey, + using: dependencies ) } @@ -392,11 +397,15 @@ public final class OpenGroupManager { // If it's a session-run room then just set it to inactive _ = try? OpenGroup .filter(id: openGroupId) - .updateAllAndConfig(db, OpenGroup.Columns.isActive.set(to: false)) + .updateAllAndConfig( + db, + OpenGroup.Columns.isActive.set(to: false), + using: dependencies + ) } if !calledFromConfigHandling, let server: String = server, let roomToken: String = roomToken { - try? LibSession.remove(db, server: server, roomToken: roomToken) + try? LibSession.remove(db, server: server, roomToken: roomToken, using: dependencies) } } @@ -475,7 +484,7 @@ public final class OpenGroupManager { try OpenGroup .filter(id: openGroup.id) - .updateAllAndConfig(db, changes) + .updateAllAndConfig(db, changes, using: dependencies) // Update the admin/moderator group members if let roomDetails: OpenGroupAPI.Room = pollInfo.details { @@ -761,7 +770,7 @@ public final class OpenGroupManager { switch processedMessage { case .config, .none: break - case .standard(let threadId, _, let proto, let messageInfo): + case .standard(_, _, let proto, let messageInfo): // We want to update the BlindedIdLookup cache with the message info so we can avoid using the // "expensive" lookup when possible let lookup: BlindedIdLookup = try { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 127ea94ac28..10d5c3c3892 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -25,7 +25,7 @@ extension MessageReceiver { case .provisionalAnswer: break // TODO: Implement case let .iceCandidates(sdpMLineIndexes, sdpMids): - SessionEnvironment.shared?.callManager.wrappedValue?.handleICECandidates( + Singleton.callManager.handleICECandidates( message: message, sdpMLineIndexes: sdpMLineIndexes, sdpMids: sdpMids @@ -60,8 +60,14 @@ extension MessageReceiver { guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestampMs: timestamp) else { // Add missed call message for call offer messages from more than one minute if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .missed, using: dependencies) { - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: sender, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) if !interaction.wasRead { SessionEnvironment.shared?.notificationsManager.wrappedValue? @@ -81,8 +87,14 @@ extension MessageReceiver { let state: CallMessage.MessageInfo.State = (db[.areCallsEnabled] ? .permissionDeniedMicrophone : .permissionDenied) if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: state, using: dependencies) { - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: sender, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) if !interaction.wasRead { SessionEnvironment.shared?.notificationsManager.wrappedValue? @@ -104,15 +116,13 @@ extension MessageReceiver { return } - // Ensure we have a call manager before continuing - guard let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue else { return } - // Ignore pre offer message after the same call instance has been generated - if let currentCall: CurrentCallProtocol = callManager.currentCall, currentCall.uuid == message.uuid { + if let currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid { + SNLog("[MessageReceiver+Calls] Ignoring pre-offer message for call[\(currentCall.uuid)] instance because it is already active.") return } - guard callManager.currentCall == nil else { + guard Singleton.callManager.currentCall == nil else { try MessageReceiver.handleIncomingCallOfferInBusyState(db, message: message) return } @@ -120,7 +130,7 @@ extension MessageReceiver { let interaction: Interaction? = try MessageReceiver.insertCallInfoMessage(db, for: message, using: dependencies) // Handle UI - callManager.showCallUIForCall( + Singleton.callManager.showCallUIForCall( caller: sender, uuid: message.uuid, mode: .answer, @@ -133,8 +143,7 @@ extension MessageReceiver { // Ensure we have a call manager before continuing guard - let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue, - let currentCall: CurrentCallProtocol = callManager.currentCall, + let currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid, let sdp: String = message.sdps.first else { return } @@ -147,9 +156,8 @@ extension MessageReceiver { SNLog("[Calls] Received answer message.") guard - let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue, - callManager.currentWebRTCSessionMatches(callId: message.uuid), - var currentCall: CurrentCallProtocol = callManager.currentCall, + Singleton.callManager.currentWebRTCSessionMatches(callId: message.uuid), + var currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid, let sender: String = message.sender else { return } @@ -157,8 +165,8 @@ extension MessageReceiver { guard sender != getUserHexEncodedPublicKey(db) else { guard !currentCall.hasStartedConnecting else { return } - callManager.dismissAllCallUI() - callManager.reportCurrentCallEnded(reason: .answeredElsewhere) + Singleton.callManager.dismissAllCallUI() + Singleton.callManager.reportCurrentCallEnded(reason: .answeredElsewhere) return } guard let sdp: String = message.sdps.first else { return } @@ -166,22 +174,21 @@ extension MessageReceiver { let sdpDescription: RTCSessionDescription = RTCSessionDescription(type: .answer, sdp: sdp) currentCall.hasStartedConnecting = true currentCall.didReceiveRemoteSDP(sdp: sdpDescription) - callManager.handleAnswerMessage(message) + Singleton.callManager.handleAnswerMessage(message) } private static func handleEndCallMessage(_ db: Database, message: CallMessage) { SNLog("[Calls] Received end call message.") guard - let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue, - callManager.currentWebRTCSessionMatches(callId: message.uuid), - let currentCall: CurrentCallProtocol = callManager.currentCall, + Singleton.callManager.currentWebRTCSessionMatches(callId: message.uuid), + let currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid, let sender: String = message.sender else { return } - callManager.dismissAllCallUI() - callManager.reportCurrentCallEnded( + Singleton.callManager.dismissAllCallUI() + Singleton.callManager.reportCurrentCallEnded( reason: (sender == getUserHexEncodedPublicKey(db) ? .declinedElsewhere : .remoteEnded diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 2ecf4aa9406..79750f55c1c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -31,7 +31,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .membersAdded: @@ -39,7 +40,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .membersRemoved: @@ -47,7 +49,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .memberLeft: @@ -55,7 +58,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .encryptionKeyPairRequest: break // Currently not used @@ -117,7 +121,7 @@ extension MessageReceiver { admins: adminsAsData.map { $0.toHexString() }, expirationTimer: expirationTimer, formationTimestampMs: sentTimestamp, - calledFromConfigHandling: false, + calledFromConfig: nil, using: dependencies ) } @@ -131,7 +135,7 @@ extension MessageReceiver { admins: [String], expirationTimer: UInt32, formationTimestampMs: UInt64, - calledFromConfigHandling: Bool, + calledFromConfig configTriggeringChange: LibSession.Config.Variant?, using dependencies: Dependencies ) throws { // With new closed groups we only want to create them if the admin creating the closed group is an @@ -149,17 +153,20 @@ extension MessageReceiver { // If the group came from the updated config handling then it doesn't matter if we // have an approved admin - we should add it regardless (as it's been synced from // antoher device) - guard hasApprovedAdmin || calledFromConfigHandling else { return } + guard hasApprovedAdmin || configTriggeringChange != nil else { return } // Create the group - let thread: SessionThread = try SessionThread - .fetchOrCreate( - db, - id: groupPublicKey, - variant: .legacyGroup, + let thread: SessionThread = try SessionThread.upsert( + db, + id: groupPublicKey, + variant: .legacyGroup, + values: SessionThread.TargetValues( creationDateTimestamp: (TimeInterval(formationTimestampMs) / 1000), - shouldBeVisible: true - ) + shouldBeVisible: .setTo(true) + ), + calledFromConfig: configTriggeringChange, + using: dependencies + ) let closedGroup: ClosedGroup = try ClosedGroup( threadId: groupPublicKey, name: name, @@ -219,7 +226,7 @@ extension MessageReceiver { try newKeyPair.insert(db) } - if !calledFromConfigHandling { + if configTriggeringChange == nil { // Update libSession try? LibSession.add( db, @@ -231,7 +238,8 @@ extension MessageReceiver { latestKeyPairReceivedTimestamp: receivedTimestamp, disappearingConfig: disappearingConfig, members: members.asSet(), - admins: admins.asSet() + admins: admins.asSet(), + using: dependencies ) } @@ -333,7 +341,8 @@ extension MessageReceiver { try? LibSession.update( db, groupPublicKey: groupPublicKey, - latestKeyPair: keyPair + latestKeyPair: keyPair, + using: dependencies ) } catch { @@ -351,7 +360,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -370,7 +380,8 @@ extension MessageReceiver { try? LibSession.update( db, groupPublicKey: threadId, - name: name + name: name, + using: dependencies ) _ = try ClosedGroup @@ -387,7 +398,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -421,7 +433,8 @@ extension MessageReceiver { admins: allMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Create records for any new members @@ -476,7 +489,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -528,7 +542,8 @@ extension MessageReceiver { admins: allMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Delete the removed members @@ -549,7 +564,8 @@ extension MessageReceiver { db, threadId: threadId, removeGroupData: true, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -564,7 +580,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -603,7 +620,8 @@ extension MessageReceiver { admins: allMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Delete the members to remove @@ -617,7 +635,8 @@ extension MessageReceiver { db, threadId: threadId, removeGroupData: (sender == userPublicKey), - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift index 44f2c0f5b9e..74ff4272e5a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift @@ -12,7 +12,8 @@ extension MessageReceiver { threadVariant: SessionThread.Variant, message: ExpirationTimerUpdate, serverExpirationTimestamp: TimeInterval?, - proto: SNProtoContent + proto: SNProtoContent, + using dependencies: Dependencies ) throws { guard proto.hasExpirationType || proto.hasExpirationTimer else { return } guard @@ -52,7 +53,8 @@ extension MessageReceiver { .update( db, groupPublicKey: threadId, - disappearingConfig: updatedConfig + disappearingConfig: updatedConfig, + using: dependencies ) } fallthrough @@ -81,7 +83,8 @@ extension MessageReceiver { _ db: Database, messageVariant: Message.Variant?, contactId: String?, - version: FeatureVersion? + version: FeatureVersion?, + using dependencies: Dependencies ) { guard let messageVariant: Message.Variant = messageVariant, @@ -97,7 +100,8 @@ extension MessageReceiver { .filter(id: contactId) .updateAllAndConfig( db, - Contact.Columns.lastKnownClientVersion.set(to: version) + Contact.Columns.lastKnownClientVersion.set(to: version), + using: dependencies ) } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index c9d240fbfa6..9b04338db98 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -50,8 +50,14 @@ extension MessageReceiver { } // Prep the unblinded thread - let unblindedThread: SessionThread = try SessionThread - .fetchOrCreate(db, id: senderId, variant: .contact, shouldBeVisible: nil) + let unblindedThread: SessionThread = try SessionThread.upsert( + db, + id: senderId, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) // Need to handle a `MessageRequestResponse` sent to a blinded thread (ie. check if the sender matches // the blinded ids of any threads) @@ -60,12 +66,12 @@ extension MessageReceiver { .filter(SessionThread.Columns.variant == SessionThread.Variant.contact) .filter( ( - ClosedGroup.Columns.threadId > SessionId.Prefix.blinded15.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.blinded15.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.blinded15.rawValue && + SessionThread.Columns.id < SessionId.Prefix.blinded15.endOfRangeString ) || ( - ClosedGroup.Columns.threadId > SessionId.Prefix.blinded25.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.blinded25.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.blinded25.rawValue && + SessionThread.Columns.id < SessionId.Prefix.blinded25.endOfRangeString ) ) .asRequest(of: String.self) @@ -112,7 +118,8 @@ extension MessageReceiver { db, type: .deleteContactConversationAndContact, // Blinded contact isn't synced anyway threadId: blindedIdLookup.blindedId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -120,7 +127,8 @@ extension MessageReceiver { try updateContactApprovalStatusIfNeeded( db, senderSessionId: senderId, - threadId: nil + threadId: nil, + using: dependencies ) // If there were blinded contacts which have now been resolved to this contact then we should remove @@ -134,7 +142,8 @@ extension MessageReceiver { try updateContactApprovalStatusIfNeeded( db, senderSessionId: userPublicKey, - threadId: unblindedThread.id + threadId: unblindedThread.id, + using: dependencies ) } @@ -159,7 +168,8 @@ extension MessageReceiver { internal static func updateContactApprovalStatusIfNeeded( _ db: Database, senderSessionId: String, - threadId: String? + threadId: String?, + using dependencies: Dependencies ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -182,7 +192,11 @@ extension MessageReceiver { try? contact.save(db) _ = try? Contact .filter(id: threadId) - .updateAllAndConfig(db, Contact.Columns.isApproved.set(to: true)) + .updateAllAndConfig( + db, + Contact.Columns.isApproved.set(to: true), + using: dependencies + ) } else { // The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to @@ -194,7 +208,11 @@ extension MessageReceiver { try? contact.save(db) _ = try? Contact .filter(id: senderSessionId) - .updateAllAndConfig(db, Contact.Columns.didApproveMe.set(to: true)) + .updateAllAndConfig( + db, + Contact.Columns.didApproveMe.set(to: true), + using: dependencies + ) } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index 8f7d7065c93..792a7df90e6 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -10,7 +10,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: UnsendRequest + message: UnsendRequest, + using dependencies: Dependencies ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -27,7 +28,7 @@ extension MessageReceiver { let interaction: Interaction = maybeInteraction else { return } - // Mark incoming messages as read and remove any of their notifications + /// Mark incoming messages as read and remove any of their notifications if interaction.variant == .standardIncoming { try Interaction.markAsRead( db, @@ -35,23 +36,20 @@ extension MessageReceiver { threadId: interaction.threadId, threadVariant: threadVariant, includingOlder: false, - trySendReadReceipt: false + trySendReadReceipt: false, + using: dependencies ) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: interaction.notificationIdentifiers) UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: interaction.notificationIdentifiers) } - if author == message.sender, let serverHash: String = interaction.serverHash { - SnodeAPI - .deleteMessages( - swarmPublicKey: author, - serverHashes: [serverHash] - ) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sinkUntilComplete() - } - + /// Retrieve the hashes which should be deleted first (these will be removed by marking the message as deleted) + let hashes: Set = try Interaction.serverHashesForDeletion( + db, + interactionIds: [interactionId] + ) + switch (interaction.variant, (author == message.sender)) { case (.standardOutgoing, _), (_, false): _ = try interaction.delete(db) @@ -71,5 +69,40 @@ extension MessageReceiver { ) } } + + /// Can't delete from the legacy group swarm so only bother for contact conversations + switch threadVariant { + case .legacyGroup, .group, .community: break + case .contact: + dependencies.storage + .readPublisher { db in + try SnodeAPI.preparedDeleteMessages( + db, + swarmPublicKey: userPublicKey, + serverHashes: Array(hashes), + requireSuccessfulDeletion: false, + using: dependencies + ) + } + .flatMap { $0.send(using: dependencies) } + .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .failure: break + case .finished: + /// Since the server deletion was successful we should also remove the `SnodeReceivedMessageInfo` + /// entries for the hashes (otherwise we might try to poll for a hash which no longer exists, resulting in fetching + /// the last 14 days of messages) + dependencies.storage.writeAsync { db in + try SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: Array(hashes) + ) + } + } + } + ) + } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 067a6a21384..0dd8f4c6fb7 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -13,7 +13,7 @@ extension MessageReceiver { message: VisibleMessage, serverExpirationTimestamp: TimeInterval?, associatedWithProto proto: SNProtoContent, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> Int64 { guard let sender: String = message.sender, let dataMessage = proto.dataMessage else { throw MessageReceiverError.invalidMessage @@ -67,8 +67,14 @@ extension MessageReceiver { // Store the message variant so we can run variant-specific behaviours let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: threadId, variant: threadVariant, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: threadId, + variant: threadVariant, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) let maybeOpenGroup: OpenGroup? = { guard threadVariant == .community else { return nil } @@ -206,7 +212,8 @@ extension MessageReceiver { interactionId: existingInteractionId, messageSentTimestamp: messageSentTimestamp, variant: variant, - syncTarget: message.syncTarget + syncTarget: message.syncTarget, + using: dependencies ) Message.getExpirationForOutgoingDisappearingMessages( @@ -233,7 +240,8 @@ extension MessageReceiver { interactionId: interactionId, messageSentTimestamp: messageSentTimestamp, variant: variant, - syncTarget: message.syncTarget + syncTarget: message.syncTarget, + using: dependencies ) if messageExpirationInfo.shouldUpdateExpiry { @@ -350,7 +358,8 @@ extension MessageReceiver { try MessageReceiver.updateContactApprovalStatusIfNeeded( db, senderSessionId: sender, - threadId: thread.id + threadId: thread.id, + using: dependencies ) } @@ -457,7 +466,8 @@ extension MessageReceiver { interactionId: Int64, messageSentTimestamp: TimeInterval, variant: Interaction.Variant, - syncTarget: String? + syncTarget: String?, + using dependencies: Dependencies ) throws { guard variant == .standardOutgoing else { return } @@ -481,7 +491,8 @@ extension MessageReceiver { threadId: thread.id, threadVariant: thread.variant, includingOlder: true, - trySendReadReceipt: false + trySendReadReceipt: false, + using: dependencies ) // Process any PendingReadReceipt values diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index d89d221aa1d..2d08ddc964b 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -40,8 +40,14 @@ extension MessageSender { let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) // Create the relevant objects in the database - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true) + let thread: SessionThread = try SessionThread.upsert( + db, + id: groupPublicKey, + variant: .legacyGroup, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: nil, + using: dependencies + ) try ClosedGroup( threadId: groupPublicKey, name: name, @@ -87,7 +93,8 @@ extension MessageSender { latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp, disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey), members: members, - admins: admins + admins: admins, + using: dependencies ) let memberSendData: [MessageSender.PreparedSendData] = try members @@ -268,7 +275,8 @@ extension MessageSender { admins: allGroupMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) } @@ -338,7 +346,8 @@ extension MessageSender { try? LibSession.update( db, groupPublicKey: closedGroup.threadId, - name: name + name: name, + using: dependencies ) } @@ -451,7 +460,8 @@ extension MessageSender { admins: allGroupMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Send the update to the group @@ -468,7 +478,14 @@ extension MessageSender { try addedMembers.forEach { member in // Send updates to the new members individually - try SessionThread.fetchOrCreate(db, id: member, variant: .contact, shouldBeVisible: nil) + try SessionThread.upsert( + db, + id: member, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) try MessageSender.send( db, @@ -675,8 +692,14 @@ extension MessageSender { privateKey: keyPair.secretKey ).build() let plaintext = try proto.serializedData() - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: publicKey, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: publicKey, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) let ciphertext = try dependencies.crypto.tryGenerate( .ciphertextWithSessionProtocol( db, diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 6b699fc8fd1..9caeaaa5bbc 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -282,7 +282,8 @@ public enum MessageReceiver { version: ((!proto.hasExpirationType && !proto.hasExpirationTimer) ? .legacyDisappearingMessages : .newDisappearingMessages - ) + ), + using: dependencies ) switch message { @@ -327,7 +328,8 @@ public enum MessageReceiver { threadVariant: threadVariant, message: message, serverExpirationTimestamp: serverExpirationTimestamp, - proto: proto + proto: proto, + using: dependencies ) case let message as UnsendRequest: @@ -335,7 +337,8 @@ public enum MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case let message as CallMessage: @@ -361,21 +364,29 @@ public enum MessageReceiver { threadVariant: threadVariant, message: message, serverExpirationTimestamp: serverExpirationTimestamp, - associatedWithProto: proto + associatedWithProto: proto, + using: dependencies ) default: throw MessageReceiverError.unknownMessage } // Perform any required post-handling logic - try MessageReceiver.postHandleMessage(db, threadId: threadId, threadVariant: threadVariant, message: message) + try MessageReceiver.postHandleMessage( + db, + threadId: threadId, + threadVariant: threadVariant, + message: message, + using: dependencies + ) } public static func postHandleMessage( _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: Message + message: Message, + using dependencies: Dependencies ) throws { // When handling any message type which has related UI we want to make sure the thread becomes // visible (the only other spot this flag gets set is when sending messages) @@ -424,7 +435,8 @@ public enum MessageReceiver { .updateAllAndConfig( db, SessionThread.Columns.shouldBeVisible.set(to: true), - SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority), + using: dependencies ) } } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index ebd62cc1cd8..5d27f601f67 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -340,7 +340,8 @@ public enum PushNotificationAPI { receiveCompletion: { result in switch result { case .finished: break - case .failure: Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups.") + case .failure(let error): + Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups due to error: \(error).") } } ) diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index 1f11a14570c..8e8f21957fb 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -413,7 +413,9 @@ public class Poller { job: job, canStartJob: ( !forceSynchronousProcessing && - (Singleton.hasAppContext && !Singleton.appContext.isInBackground) + (Singleton.hasAppContext && !Singleton.appContext.isInBackground) || + // FIXME: Better seperate the call messages handling, since we need to handle them all the time + Singleton.callManager.currentCall != nil ), using: dependencies ) diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 5362239f559..b343d7c5acc 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -272,7 +272,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat } /// This method marks a thread as read and depending on the target may also update the interactions within a thread as read - public func markAsRead(target: ReadTarget) { + public func markAsRead(target: ReadTarget, using dependencies: Dependencies) { // Store the logic to mark a thread as read (to paths need to run this) let threadId: String = self.threadId let threadWasMarkedUnread: Bool? = self.threadWasMarkedUnread @@ -286,7 +286,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat .filter(id: threadId) .updateAllAndConfig( db, - SessionThread.Columns.markedAsUnread.set(to: false) + SessionThread.Columns.markedAsUnread.set(to: false), + using: dependencies ) } } @@ -327,14 +328,15 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat threadVariant: threadVariant, isBlocked: threadIsBlocked, isMessageRequest: threadIsMessageRequest - ) + ), + using: dependencies ) } } } /// This method will mark a thread as read - public func markAsUnread() { + public func markAsUnread(using dependencies: Dependencies) { guard self.threadWasMarkedUnread != true else { return } let threadId: String = self.threadId @@ -344,7 +346,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat .filter(id: threadId) .updateAllAndConfig( db, - SessionThread.Columns.markedAsUnread.set(to: true) + SessionThread.Columns.markedAsUnread.set(to: true), + using: dependencies ) } } diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index c1b15ce893e..3331481c2e3 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -73,6 +73,10 @@ public extension Setting.BoolKey { /// Controls whether the device will poll for community message requests (SOGS `/inbox` endpoint) static let checkForCommunityMessageRequests: Setting.BoolKey = "checkForCommunityMessageRequests" + + /// Controls whether developer mode is enabled (this displays a section within the Settings screen which allows manual control of feature flags + /// and system settings for better debugging) + static let developerModeEnabled: Setting.BoolKey = "developerModeEnabled" } // stringlint:ignore_contents diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index 5b6f3b752c8..e54bd7705fe 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -605,7 +605,7 @@ public struct ProfileManager { else { try Profile .filter(id: publicKey) - .updateAllAndConfig(db, profileChanges) + .updateAllAndConfig(db, profileChanges, using: dependencies) } } diff --git a/SessionMessagingKit/Utilities/SessionEnvironment.swift b/SessionMessagingKit/Utilities/SessionEnvironment.swift index 8c356391e6c..65e8ee3bac2 100644 --- a/SessionMessagingKit/Utilities/SessionEnvironment.swift +++ b/SessionMessagingKit/Utilities/SessionEnvironment.swift @@ -11,9 +11,6 @@ public class SessionEnvironment { public let windowManager: OWSWindowManager public var isRequestingPermission: Bool - // Note: This property is configured after Environment is created. - public let callManager: Atomic = Atomic(nil) - // Note: This property is configured after Environment is created. public let notificationsManager: Atomic = Atomic(nil) diff --git a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift index e2ab1137841..d3e231d3f3d 100644 --- a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift @@ -9,7 +9,7 @@ import Nimble @testable import SessionMessagingKit @testable import SessionUtilitiesKit -extension Job: MutableIdentifiable { +extension Job: @retroactive MutableIdentifiable { public mutating func setId(_ id: Int64?) { self.id = id } } @@ -63,11 +63,16 @@ class MessageSendJobSpec: QuickSpec { SNMessagingKit.self ], initialData: { db in - try SessionThread.fetchOrCreate( + try SessionThread.upsert( db, id: "Test1", variant: .contact, - shouldBeVisible: true + values: SessionThread.TargetValues( + // False is the default and will mean we don't need libSession loaded + shouldBeVisible: .setTo(false) + ), + calledFromConfig: nil, + using: dependencies ) }, using: dependencies diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index f48291489eb..39c5a57bf2b 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -19,7 +19,7 @@ class OpenGroupManagerSpec: QuickSpec { id: 234, serverHash: "TestServerHash", messageUuid: nil, - threadId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + threadId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), authorId: "TestAuthorId", variant: .standardOutgoing, body: "Test", @@ -39,12 +39,12 @@ class OpenGroupManagerSpec: QuickSpec { mostRecentFailureText: nil ) @TestState var testGroupThread: SessionThread! = SessionThread( - id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + id: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), variant: .community, creationDateTimestamp: 0 ) @TestState var testOpenGroup: OpenGroup! = OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -210,6 +210,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- cache data context("cache data") { // MARK: ---- defaults the time since last open to greatestFiniteMagnitude @@ -301,7 +309,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.getOrCreatePoller(for: "testserver") + $0.getOrCreatePoller(for: "http://127.0.0.1") }) expect(mockOGMCache) .to(call(matchingParameters: true) { @@ -437,7 +445,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- when there is a thread for the room and the cache has a poller context("when there is a thread for the room and the cache has a poller") { beforeEach { - mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://127.0.0.1"]) } // MARK: ------ for the no-scheme variant @@ -450,7 +458,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -466,7 +474,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "http://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -482,7 +490,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "https://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -501,7 +509,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -517,7 +525,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "http://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -533,7 +541,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "https://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -552,7 +560,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -568,7 +576,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "http://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -584,7 +592,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "https://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -688,7 +696,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -708,7 +716,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -720,6 +728,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when adding context("when adding") { beforeEach { @@ -746,9 +762,13 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } @@ -756,7 +776,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -773,7 +793,7 @@ class OpenGroupManagerSpec: QuickSpec { .fetchOne(db) } ) - .to(equal(OpenGroup.idFor(roomToken: "testRoom", server: "testServer"))) + .to(equal(OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"))) } // MARK: ---- adds a poller @@ -784,9 +804,13 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } @@ -794,7 +818,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -804,7 +828,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.getOrCreatePoller(for: "testserver") + $0.getOrCreatePoller(for: "http://127.0.0.1") }) expect(mockPoller) .to(call(matchingParameters: true) { [dependencies = dependencies!] in @@ -815,7 +839,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- an existing room context("an existing room") { beforeEach { - mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://127.0.0.1"]) mockStorage.write { db in try testOpenGroup.insert(db) } @@ -829,11 +853,15 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey .replacingOccurrences(of: "c3", with: "00") .replacingOccurrences(of: "b3", with: "00"), - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } @@ -841,7 +869,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey .replacingOccurrences(of: "c3", with: "00") .replacingOccurrences(of: "b3", with: "00"), @@ -896,9 +924,13 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } @@ -906,7 +938,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -934,7 +966,7 @@ class OpenGroupManagerSpec: QuickSpec { .updateAll( db, Interaction.Columns.threadId - .set(to: OpenGroup.idFor(roomToken: "testRoom", server: "testServer")) + .set(to: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1")) ) } } @@ -945,7 +977,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -961,7 +993,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -979,13 +1011,13 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) } - expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemovePoller(for: "testserver") }) + expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemovePoller(for: "http://127.0.0.1") }) } // MARK: ------ removes the open group @@ -994,7 +1026,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -1012,7 +1044,7 @@ class OpenGroupManagerSpec: QuickSpec { try OpenGroup.deleteAll(db) try testOpenGroup.insert(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom1", publicKey: TestConstants.publicKey, isActive: true, @@ -1035,7 +1067,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -1133,7 +1165,7 @@ class OpenGroupManagerSpec: QuickSpec { .handleCapabilities( db, capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []), - on: "testserver" + on: "http://127.0.0.1" ) } } @@ -1148,6 +1180,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when handling room poll info context("when handling room poll info") { beforeEach { @@ -1181,7 +1221,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies )// { didComplete = true } } @@ -1206,7 +1246,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) { didCallComplete = true } } @@ -1224,7 +1264,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) { didCallComplete = true } @@ -1240,7 +1280,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -1254,7 +1294,7 @@ class OpenGroupManagerSpec: QuickSpec { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ - OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Just(Data()).setFailureType(to: Error.self).eraseToAnyPublisher() + OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): Just(Data()).setFailureType(to: Error.self).eraseToAnyPublisher() ]) mockStorage.write { db in @@ -1263,7 +1303,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) { didCallComplete = true } @@ -1293,7 +1333,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1303,7 +1343,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1311,7 +1351,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestMod", role: .moderator, @@ -1339,7 +1379,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1349,7 +1389,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1357,7 +1397,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestMod2", role: .moderator, @@ -1379,7 +1419,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1410,7 +1450,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1420,7 +1460,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1428,7 +1468,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestAdmin", role: .admin, @@ -1456,7 +1496,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1466,7 +1506,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1474,7 +1514,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestAdmin2", role: .admin, @@ -1497,7 +1537,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1521,7 +1561,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1540,7 +1580,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: nil, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1573,7 +1613,7 @@ class OpenGroupManagerSpec: QuickSpec { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ - OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Just(imageData).setFailureType(to: Error.self).eraseToAnyPublisher() + OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): Just(imageData).setFailureType(to: Error.self).eraseToAnyPublisher() ]) } @@ -1595,7 +1635,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1624,7 +1664,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -1648,7 +1688,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1677,7 +1717,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -1713,7 +1753,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1746,7 +1786,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1766,7 +1806,7 @@ class OpenGroupManagerSpec: QuickSpec { it("does nothing if it fails to retrieve the room image") { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ - OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Fail(error: NetworkError.unknown).eraseToAnyPublisher() + OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): Fail(error: NetworkError.unknown).eraseToAnyPublisher() ]) testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( @@ -1785,7 +1825,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1819,7 +1859,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1840,6 +1880,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when handling messages context("when handling messages") { beforeEach { @@ -1872,7 +1920,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1894,7 +1942,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1935,7 +1983,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1969,7 +2017,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1984,7 +2032,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testMessage], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2015,7 +2063,7 @@ class OpenGroupManagerSpec: QuickSpec { testMessage, ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2055,7 +2103,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2085,7 +2133,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2132,7 +2180,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2166,7 +2214,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2205,7 +2253,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2228,7 +2276,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2270,7 +2318,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2285,7 +2333,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2310,7 +2358,7 @@ class OpenGroupManagerSpec: QuickSpec { testDirectMessage ], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2334,7 +2382,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2355,7 +2403,7 @@ class OpenGroupManagerSpec: QuickSpec { try BlindedIdLookup( blindedId: "15\(TestConstants.publicKey)", sessionId: "TestSessionId", - openGroupServer: "testserver", + openGroupServer: "http://127.0.0.1", openGroupPublicKey: "05\(TestConstants.publicKey)" ).insert(db) } @@ -2365,7 +2413,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2381,7 +2429,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2430,7 +2478,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2445,7 +2493,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2470,7 +2518,7 @@ class OpenGroupManagerSpec: QuickSpec { testDirectMessage ], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2483,6 +2531,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when determining if a user is a moderator or an admin context("when determining if a user is a moderator or an admin") { beforeEach { @@ -2497,7 +2553,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2509,7 +2565,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2519,7 +2575,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the key is in the moderator set") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .moderator, isHidden: false @@ -2530,7 +2586,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2540,7 +2596,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the key is in the admin set") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .admin, isHidden: false @@ -2551,7 +2607,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2561,7 +2617,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the moderator is hidden") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .moderator, isHidden: true @@ -2572,7 +2628,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2582,7 +2638,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the admin is hidden") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .admin, isHidden: true @@ -2593,7 +2649,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2605,7 +2661,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "InvalidValue", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2626,7 +2682,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2638,7 +2694,7 @@ class OpenGroupManagerSpec: QuickSpec { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "00\(otherKey)", role: .moderator, isHidden: false @@ -2652,7 +2708,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2671,7 +2727,7 @@ class OpenGroupManagerSpec: QuickSpec { ) mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "15\(otherKey)", role: .moderator, isHidden: false @@ -2682,7 +2738,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2702,7 +2758,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2721,7 +2777,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2733,7 +2789,7 @@ class OpenGroupManagerSpec: QuickSpec { mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(otherKey)") mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(otherKey)", role: .moderator, isHidden: false @@ -2749,7 +2805,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2768,7 +2824,7 @@ class OpenGroupManagerSpec: QuickSpec { ) mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "15\(otherKey)", role: .moderator, isHidden: false @@ -2784,7 +2840,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2804,7 +2860,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2820,7 +2876,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2842,7 +2898,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2862,7 +2918,7 @@ class OpenGroupManagerSpec: QuickSpec { ) mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(otherKey)", role: .moderator, isHidden: false @@ -2878,7 +2934,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2898,7 +2954,7 @@ class OpenGroupManagerSpec: QuickSpec { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "00\(otherKey)", role: .moderator, isHidden: false @@ -2914,7 +2970,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -3183,14 +3239,14 @@ class OpenGroupManagerSpec: QuickSpec { .eraseToAnyPublisher() mockOGMCache .when { $0.groupImagePublishers } - .thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): publisher]) + .thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): publisher]) var result: Data? OpenGroupManager .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3206,7 +3262,7 @@ class OpenGroupManagerSpec: QuickSpec { .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3216,7 +3272,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.read { db -> Data? in try OpenGroup .select(.imageData) - .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer")) + .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1")) .asRequest(of: Data.self) .fetchOne(db) } @@ -3229,7 +3285,7 @@ class OpenGroupManagerSpec: QuickSpec { .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3250,7 +3306,7 @@ class OpenGroupManagerSpec: QuickSpec { .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3258,7 +3314,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.groupImagePublishers = [OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): publisher] + $0.groupImagePublishers = [OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): publisher] }) } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 2ee25b6b184..39d31ebd0d8 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -164,13 +164,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension using: dependencies ) { - let thread: SessionThread = try SessionThread - .fetchOrCreate( - db, - id: sender, - variant: .contact, - shouldBeVisible: nil - ) + let thread: SessionThread = try SessionThread.upsert( + db, + id: sender, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: nil, + using: dependencies + ) // Notify the user if the call message wasn't already read if !interaction.wasRead { @@ -198,7 +199,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension db, threadId: threadId, threadVariant: threadVariant, - message: messageInfo.message + message: messageInfo.message, + using: dependencies ) return self?.handleSuccessForIncomingCall(db, for: callMessage) @@ -209,7 +211,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension db, threadId: threadId, threadVariant: threadVariant, - message: messageInfo.message + message: messageInfo.message, + using: dependencies ) case .standard(let threadId, let threadVariant, let proto, let messageInfo): diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 94cee744bce..8c9e9add1cc 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -274,7 +274,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView .updateAllAndConfig( db, SessionThread.Columns.shouldBeVisible.set(to: true), - SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority), + using: dependencies ) } diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index 5358ecf0e6c..91cf146ade0 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -413,9 +413,6 @@ public final class SnodeAPI { let sendTimestamp: UInt64 = UInt64(SnodeAPI.currentOffsetTimestampMs()) - // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed - let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length - do { return try SnodeAPI .prepareRequest( @@ -683,9 +680,6 @@ public final class SnodeAPI { .eraseToAnyPublisher() } - // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed - let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length - do { return try SnodeAPI .prepareRequest( diff --git a/SessionUIKit/Components/ConfirmationModal.swift b/SessionUIKit/Components/ConfirmationModal.swift index 971c54368d2..a8256496de7 100644 --- a/SessionUIKit/Components/ConfirmationModal.swift +++ b/SessionUIKit/Components/ConfirmationModal.swift @@ -167,6 +167,10 @@ public class ConfirmationModal: Modal, UITextFieldDelegate { fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + public override func populateContentView() { let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( target: self, @@ -188,6 +192,24 @@ public class ConfirmationModal: Modal, UITextFieldDelegate { mainStackView.pin(to: contentView) closeButton.pin(.top, to: .top, of: contentView, withInset: 8) closeButton.pin(.right, to: .right, of: contentView, withInset: -8) + + // Observe keyboard notifications + let keyboardNotifications: [Notification.Name] = [ + UIResponder.keyboardWillShowNotification, + UIResponder.keyboardDidShowNotification, + UIResponder.keyboardWillChangeFrameNotification, + UIResponder.keyboardDidChangeFrameNotification, + UIResponder.keyboardWillHideNotification, + UIResponder.keyboardDidHideNotification + ] + keyboardNotifications.forEach { notification in + NotificationCenter.default.addObserver( + self, + selector: #selector(handleKeyboardNotification(_:)), + name: notification, + object: nil + ) + } } // MARK: - Content @@ -325,6 +347,67 @@ public class ConfirmationModal: Modal, UITextFieldDelegate { override public func cancel() { internalOnCancel?(self) } + + // MARK: - Keyboard Avoidance + + @objc func handleKeyboardNotification(_ notification: Notification) { + guard + let userInfo: [AnyHashable: Any] = notification.userInfo, + var keyboardEndFrame: CGRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect + else { return } + + // If reduce motion+crossfade transitions is on, in iOS 14 UIKit vends out a keyboard end frame + // of CGRect zero. This breaks the math below. + // + // If our keyboard end frame is CGRectZero, build a fake rect that's translated off the bottom edge. + if keyboardEndFrame == .zero { + keyboardEndFrame = CGRect( + x: UIScreen.main.bounds.minX, + y: UIScreen.main.bounds.maxY, + width: UIScreen.main.bounds.width, + height: 0 + ) + } + + // No nothing if there was no change +// let keyboardEndFrameConverted: CGRect = self.view.convert(keyboardEndFrame, from: nil) +// guard keyboardEndFrameConverted != lastKnownKeyboardFrame else { return } +// +// self.lastKnownKeyboardFrame = keyboardEndFrameConverted + + // Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600 + // and https://stackoverflow.com/a/25260930 to better understand what we are + // doing with the UIViewAnimationOptions + let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue)) + let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16)) + let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0) + + guard duration > 0, !UIAccessibility.isReduceMotionEnabled else { + // UIKit by default (sometimes? never?) animates all changes in response to keyboard events. + // We want to suppress those animations if the view isn't visible, + // otherwise presentation animations don't work properly. + UIView.performWithoutAnimation { + self.updateKeyboardAvoidance(keyboardEndFrame: keyboardEndFrame) + } + return + } + + UIView.animate( + withDuration: duration, + delay: 0, + options: options, + animations: { [weak self] in + self?.updateKeyboardAvoidance(keyboardEndFrame: keyboardEndFrame) + self?.view.layoutIfNeeded() + }, + completion: nil + ) + } + + private func updateKeyboardAvoidance(keyboardEndFrame: CGRect) { + contentTopConstraint?.isActive = (keyboardEndFrame.minY < (view.bounds.height - 100)) + contentCenterYConstraint?.isActive = (contentTopConstraint?.isActive != true) + } } // MARK: - Types diff --git a/SessionUIKit/Components/Modal.swift b/SessionUIKit/Components/Modal.swift index 9d52c574886..52f19d44a27 100644 --- a/SessionUIKit/Components/Modal.swift +++ b/SessionUIKit/Components/Modal.swift @@ -16,6 +16,9 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { // MARK: - Components + internal var contentTopConstraint: NSLayoutConstraint? + internal var contentCenterYConstraint: NSLayoutConstraint? + private lazy var dimmingView: UIView = { let result = UIVisualEffectView() @@ -97,7 +100,6 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { if UIDevice.current.isIPad { containerView.set(.width, to: Values.iPadModalWidth) - containerView.center(in: view) } else { containerView.leadingAnchor @@ -106,9 +108,14 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { view.trailingAnchor .constraint(equalTo: containerView.trailingAnchor, constant: Values.veryLargeSpacing) .isActive = true - containerView.center(.vertical, in: view) } + containerView.center(.horizontal, in: view) + contentCenterYConstraint = containerView.center(.vertical, in: view) + contentTopConstraint = containerView + .pin(.top, toMargin: .top, of: view) + .setting(isActive: false) + // Gestures let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close)) swipeGestureRecognizer.direction = .down diff --git a/SessionUIKit/Style Guide/Fonts.swift b/SessionUIKit/Style Guide/Fonts.swift index 8fd5213242b..4572cbf4421 100644 --- a/SessionUIKit/Style Guide/Fonts.swift +++ b/SessionUIKit/Style Guide/Fonts.swift @@ -5,6 +5,8 @@ import UIKit import SwiftUI +// MARK: - UIKit + public enum Fonts { public static func spaceMono(ofSize size: CGFloat) -> UIFont { return UIFont(name: "SpaceMono-Regular", size: size)! @@ -15,6 +17,61 @@ public enum Fonts { } } +public extension Fonts { + enum Headings { + public static let H1: UIFont = .boldSystemFont(ofSize: CGFloat(36)) + public static let H2: UIFont = .boldSystemFont(ofSize: CGFloat(32)) + public static let H3: UIFont = .boldSystemFont(ofSize: CGFloat(29)) + public static let H4: UIFont = .boldSystemFont(ofSize: CGFloat(26)) + public static let H5: UIFont = .boldSystemFont(ofSize: CGFloat(23)) + public static let H6: UIFont = .boldSystemFont(ofSize: CGFloat(20)) + public static let H7: UIFont = .boldSystemFont(ofSize: CGFloat(18)) + public static let H8: UIFont = .boldSystemFont(ofSize: CGFloat(16)) + public static let H9: UIFont = .boldSystemFont(ofSize: CGFloat(14)) + + public static func custom(_ size: CGFloat) -> UIFont { + return .boldSystemFont(ofSize: size) + } + } + + enum Body { + public static let extraLargeRegular: UIFont = .systemFont(ofSize: CGFloat(18)) + public static let largeRegular: UIFont = .systemFont(ofSize: CGFloat(16)) + public static let baseRegular: UIFont = .systemFont(ofSize: CGFloat(14)) + public static let smallRegular: UIFont = .systemFont(ofSize: CGFloat(12)) + public static let extraSmallRegular: UIFont = .systemFont(ofSize: CGFloat(11)) + public static let finePrintRegular: UIFont = .systemFont(ofSize: CGFloat(9)) + public static let extraLargeBold: UIFont = .boldSystemFont(ofSize: CGFloat(18)) + public static let largeBold: UIFont = .boldSystemFont(ofSize: CGFloat(16)) + public static let baseBold: UIFont = .boldSystemFont(ofSize: CGFloat(14)) + public static let smallBold: UIFont = .boldSystemFont(ofSize: CGFloat(12)) + public static let extraSmallBold: UIFont = .boldSystemFont(ofSize: CGFloat(11)) + public static let finePrintBold: UIFont = .boldSystemFont(ofSize: CGFloat(9)) + + public static func custom(_ size: CGFloat, bold: Bool = false) -> UIFont { + switch bold { + case true: return .boldSystemFont(ofSize: size) + case false: return .systemFont(ofSize: size) + } + } + } + + enum Display { + public static let extraLarge: UIFont = Fonts.spaceMono(ofSize: CGFloat(18)) + public static let large: UIFont = Fonts.spaceMono(ofSize: CGFloat(16)) + public static let base: UIFont = Fonts.spaceMono(ofSize: CGFloat(14)) + public static let small: UIFont = Fonts.spaceMono(ofSize: CGFloat(12)) + public static let extraSmall: UIFont = Fonts.spaceMono(ofSize: CGFloat(11)) + public static let finePrint: UIFont = Fonts.spaceMono(ofSize: CGFloat(9)) + + public static func custom(_ size: CGFloat) -> UIFont { + return Fonts.spaceMono(ofSize: size) + } + } +} + +// MARK: - SwiftUI + public extension Font { static func spaceMono(size: CGFloat) -> Font { return Font.custom("SpaceMono-Regular", size: size) @@ -24,3 +81,53 @@ public extension Font { return Font.custom("SpaceMono-Bold", size: size) } } + +public extension Font { + enum Headings { + public static let H1: Font = .system(size: CGFloat(36)).bold() + public static let H2: Font = .system(size: CGFloat(32)).bold() + public static let H3: Font = .system(size: CGFloat(29)).bold() + public static let H4: Font = .system(size: CGFloat(26)).bold() + public static let H5: Font = .system(size: CGFloat(23)).bold() + public static let H6: Font = .system(size: CGFloat(20)).bold() + public static let H7: Font = .system(size: CGFloat(18)).bold() + public static let H8: Font = .system(size: CGFloat(16)).bold() + public static let H9: Font = .system(size: CGFloat(14)).bold() + + public static func custom(_ size: CGFloat) -> Font { + return .system(size: size).bold() + } + } + + enum Body { + public static let extraLargeRegular: Font = .system(size: CGFloat(18)) + public static let largeRegular: Font = .system(size: CGFloat(16)) + public static let baseRegular: Font = .system(size: CGFloat(14)) + public static let smallRegular: Font = .system(size: CGFloat(12)) + public static let extraSmallRegular: Font = .system(size: CGFloat(11)) + public static let finePrintRegular: Font = .system(size: CGFloat(9)) + public static let extraLargeBold: Font = .system(size: CGFloat(18)).bold() + public static let largeBold: Font = .system(size: CGFloat(16)).bold() + public static let baseBold: Font = .system(size: CGFloat(14)).bold() + public static let smallBold: Font = .system(size: CGFloat(12)).bold() + public static let extraSmallBold: Font = .system(size: CGFloat(11)).bold() + public static let finePrintBold: Font = .system(size: CGFloat(9)).bold() + + public static func custom(_ size: CGFloat, bold: Bool = false) -> Font { + return .system(size: size, weight: (bold ? .bold : .regular)) + } + } + + enum Display { + public static let extraLarge: Font = .spaceMono(size: CGFloat(18)) + public static let large: Font = .spaceMono(size: CGFloat(16)) + public static let base: Font = .spaceMono(size: CGFloat(14)) + public static let small: Font = .spaceMono(size: CGFloat(12)) + public static let extraSmall: Font = .spaceMono(size: CGFloat(11)) + public static let finePrint: Font = .spaceMono(size: CGFloat(9)) + + public static func custom(_ size: CGFloat) -> Font { + return .spaceMono(size: size) + } + } +} diff --git a/SessionUIKit/Utilities/Localization+Style.swift b/SessionUIKit/Utilities/Localization+Style.swift index 1da248a26a7..03bbfae3497 100644 --- a/SessionUIKit/Utilities/Localization+Style.swift +++ b/SessionUIKit/Utilities/Localization+Style.swift @@ -4,6 +4,7 @@ import UIKit import SessionUtilitiesKit +import Lucide public extension NSAttributedString { /// These are the tags we current support formatting for @@ -19,6 +20,7 @@ public extension NSAttributedString { case underline = "u" case strikethrough = "s" case primaryTheme = "span" + case icon = "icon" // MARK: - Functions @@ -51,6 +53,7 @@ public extension NSAttributedString { case .underline: return [.underlineStyle: NSUnderlineStyle.single.rawValue] case .strikethrough: return [.strikethroughStyle: NSUnderlineStyle.single.rawValue] case .primaryTheme: return [.foregroundColor: ThemeManager.currentTheme.color(for: .sessionButton_text).defaulting(to: ThemeManager.primaryColor.color)] + case .icon: return Lucide.attributes(for: font) } } } @@ -175,6 +178,9 @@ private extension Collection where Element == NSAttributedString.HTMLTag { case .underline: result[.underlineStyle] = NSUnderlineStyle.single.rawValue case .strikethrough: result[.strikethroughStyle] = NSUnderlineStyle.single.rawValue case .primaryTheme: result[.foregroundColor] = ThemeManager.currentTheme.color(for: .sessionButton_text).defaulting(to: ThemeManager.primaryColor.color) + case .icon: + result[.font] = fontWith(Lucide.font(ofSize: (font.pointSize + 1)), traits: []) + result[.baselineOffset] = -Lucide.defaultBaselineOffset } } } diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 82352e7be57..65f87eaef4d 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -15,7 +15,7 @@ public extension KeychainStorage.DataKey { static let dbCipherKeySpec: Self = "G open class Storage { public static let queuePrefix: String = "SessionDatabase" - private static let dbFileName: String = "Session.sqlite" + public static let dbFileName: String = "Session.sqlite" private static let kSQLCipherKeySpecLength: Int = 48 /// If a transaction takes longer than this duration a warning will be logged but the transaction will continue to run @@ -69,6 +69,18 @@ open class Storage { configureDatabase(customWriter: customWriter) } + public init( + testAccessTo databasePath: String, + encryptedKeyPath: String, + encryptedKeyPassword: String + ) throws { + try testAccess( + databasePath: databasePath, + encryptedKeyPath: encryptedKeyPath, + encryptedKeyPassword: encryptedKeyPassword + ) + } + private func configureDatabase(customWriter: DatabaseWriter? = nil) { // Create the database directory if needed and ensure it's protection level is set before attempting to // create the database KeySpec or the database itself @@ -446,6 +458,16 @@ open class Storage { Log.info("[Storage] Database access resumed.") } + public func checkpoint(_ mode: Database.CheckpointMode) throws { + try dbWriter?.writeWithoutTransaction { db in _ = try db.checkpoint(mode) } + } + + public func closeDatabase() throws { + suspendDatabaseAccess() + isValid = false + dbWriter = nil + } + public func resetAllStorage() { isValid = false Storage.internalHasCreatedValidInstance.mutate { $0 = false } @@ -732,9 +754,60 @@ public extension ValueObservation { // MARK: - Debug Convenience -#if DEBUG public extension Storage { - func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) { + static let encKeyFilename: String = "key.enc" + + func testAccess( + databasePath: String, + encryptedKeyPath: String, + encryptedKeyPassword: String + ) throws { + /// First we need to ensure we can decrypt the encrypted key file + do { + var tmpKeySpec: Data = try decryptSecureExportedKey( + path: encryptedKeyPath, + password: encryptedKeyPassword + ) + tmpKeySpec.resetBytes(in: 0.. String { var keySpec: Data = try getOrGenerateDatabaseKeySpec() defer { keySpec.resetBytes(in: 0.. Data { + let encKeyBase64: String = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8) + + guard + var passwordData: Data = password.data(using: .utf8), + var encKeyData: Data = Data(base64Encoded: encKeyBase64) + else { throw StorageError.generic } + defer { + // Reset content immediately after use + passwordData.resetBytes(in: 0.. Void)? + ) throws { + guard FileManager.default.fileExists(atPath: sourcePath) else { + throw ArchiveError.invalidSourcePath + } + + let sourceUrl: URL = URL(fileURLWithPath: sourcePath) + let destinationUrl: URL = URL(fileURLWithPath: destinationPath) + + // Create output stream for backup and compression + guard let outputStream: OutputStream = OutputStream(url: destinationUrl, append: false) else { + throw ArchiveError.archiveFailed + } + + outputStream.open() + defer { outputStream.close() } + + // Stream-based directory traversal and compression + let enumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( + at: sourceUrl, + includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey] + ) + let fileUrls: [URL] = (enumerator?.allObjects + .compactMap { $0 as? URL } + .filter { url -> Bool in + guard !filenamesToExclude.contains(url.lastPathComponent) else { return false } + guard + let resourceValues = try? url.resourceValues( + forKeys: [.isRegularFileKey, .isDirectoryKey] + ) + else { return true } + + return (resourceValues.isRegularFile == true) + }) + .defaulting(to: []) + var index: Int = 0 + progressChanged?(index, (fileUrls.count + additionalPaths.count), 0, 0) + + // Include the archiver version so we can validate compatibility when importing + var version: UInt32 = DirectoryArchiver.version + let versionData: [UInt8] = Array(Data(bytes: &version, count: MemoryLayout.size)) + try write(versionData, to: outputStream, blockSize: UInt8.self, password: password) + + // Store general metadata to help with validation and any other non-file related info + var fileCount: UInt32 = UInt32(fileUrls.count) + var additionalFileCount: UInt32 = UInt32(additionalPaths.count) + + let metadata: Data = ( + Data(bytes: &fileCount, count: MemoryLayout.size) + + Data(bytes: &additionalFileCount, count: MemoryLayout.size) + ) + try write(Array(metadata), to: outputStream, blockSize: UInt64.self, password: password) + + // Write the main file content + try fileUrls.forEach { url in + index += 1 + + try exportFile( + sourcePath: sourcePath, + fileURL: url, + customRelativePath: nil, + outputStream: outputStream, + password: password, + index: index, + totalFiles: (fileUrls.count + additionalPaths.count), + isExtraFile: false, + progressChanged: progressChanged + ) + } + + // Add any extra files which we want to include + try additionalPaths.forEach { path in + index += 1 + + let fileUrl: URL = URL(fileURLWithPath: path) + try exportFile( + sourcePath: sourcePath, + fileURL: fileUrl, + customRelativePath: "_extra/\(fileUrl.lastPathComponent)", + outputStream: outputStream, + password: password, + index: index, + totalFiles: (fileUrls.count + additionalPaths.count), + isExtraFile: true, + progressChanged: progressChanged + ) + } + } + + public static func unarchiveDirectory( + archivePath: String, + destinationPath: String, + password: String?, + progressChanged: ((Int, Int, UInt64, UInt64) -> Void)? + ) throws -> (paths: [String], additional: [String]) { + // Remove any old imported data as we don't want to muddy the new data + if FileManager.default.fileExists(atPath: destinationPath) { + try? FileManager.default.removeItem(atPath: destinationPath) + } + + // Create the destination directory + try FileManager.default.createDirectory( + atPath: destinationPath, + withIntermediateDirectories: true + ) + + guard + let values: URLResourceValues = try? URL(fileURLWithPath: archivePath).resourceValues( + forKeys: [.fileSizeKey] + ), + let encryptedFileSize: UInt64 = values.fileSize.map({ UInt64($0) }), + let inputStream: InputStream = InputStream(fileAtPath: archivePath) + else { throw ArchiveError.unarchiveFailed } + + inputStream.open() + defer { inputStream.close() } + + // First we need to check the version included in the export is compatible with the current one + let (versionData, _, _): ([UInt8], Int, UInt8) = try read(from: inputStream, password: password) + + guard !versionData.isEmpty else { throw ArchiveError.incompatibleVersion } + + var version: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &version) { versionBuffer in + versionData.copyBytes(to: versionBuffer) + } + + // Retrieve and process the general metadata + var metadataOffset = 0 + let (metadataBytes, _, _): ([UInt8], Int, UInt64) = try read(from: inputStream, password: password) + + guard !metadataBytes.isEmpty else { throw ArchiveError.unarchiveFailed } + + // Extract path length and path + let expectedFileCountRange: Range = metadataOffset..<(metadataOffset + MemoryLayout.size) + var expectedFileCount: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &expectedFileCount) { expectedFileCountBuffer in + metadataBytes.copyBytes(to: expectedFileCountBuffer, from: expectedFileCountRange) + } + metadataOffset += MemoryLayout.size + + let expectedAdditionalFileCountRange: Range = metadataOffset..<(metadataOffset + MemoryLayout.size) + var expectedAdditionalFileCount: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &expectedAdditionalFileCount) { expectedAdditionalFileCountBuffer in + metadataBytes.copyBytes(to: expectedAdditionalFileCountBuffer, from: expectedAdditionalFileCountRange) + } + + var filePaths: [String] = [] + var additionalFilePaths: [String] = [] + var fileAmountProcessed: UInt64 = 0 + progressChanged?(0, Int(expectedFileCount + expectedAdditionalFileCount), 0, encryptedFileSize) + while inputStream.hasBytesAvailable { + let (metadata, blockSizeBytesRead, encryptedSize): ([UInt8], Int, UInt64) = try read( + from: inputStream, + password: password + ) + fileAmountProcessed += UInt64(blockSizeBytesRead) + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) + + // Stop here if we have finished reading + guard blockSizeBytesRead > 0 else { continue } + + // Process the metadata + var offset = 0 + + // Extract path length and path + let pathLengthRange: Range = offset..<(offset + MemoryLayout.size) + var pathLength: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &pathLength) { pathLengthBuffer in + metadata.copyBytes(to: pathLengthBuffer, from: pathLengthRange) + } + offset += MemoryLayout.size + + let pathRange: Range = offset..<(offset + Int(pathLength)) + let relativePath: String = String(data: Data(metadata[pathRange]), encoding: .utf8)! + offset += Int(pathLength) + + // Extract file size + let fileSizeRange: Range = offset..<(offset + MemoryLayout.size) + var fileSize: UInt64 = 0 + _ = withUnsafeMutableBytes(of: &fileSize) { fileSizeBuffer in + metadata.copyBytes(to: fileSizeBuffer, from: fileSizeRange) + } + offset += Int(MemoryLayout.size) + + // Extract extra file flag + let isExtraFileRange: Range = offset..<(offset + MemoryLayout.size) + var isExtraFile: Bool = false + _ = withUnsafeMutableBytes(of: &isExtraFile) { isExtraFileBuffer in + metadata.copyBytes(to: isExtraFileBuffer, from: isExtraFileRange) + } + + // Construct full file path + let fullPath: String = (destinationPath as NSString).appendingPathComponent(relativePath) + try FileManager.default.createDirectory( + atPath: (fullPath as NSString).deletingLastPathComponent, + withIntermediateDirectories: true + ) + fileAmountProcessed += encryptedSize + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) + + // Read and decrypt file content + guard let outputStream: OutputStream = OutputStream(toFileAtPath: fullPath, append: false) else { + throw ArchiveError.unarchiveFailed + } + outputStream.open() + defer { outputStream.close() } + + var remainingFileSize: Int = Int(fileSize) + while remainingFileSize > 0 { + let (chunk, chunkSizeBytesRead, encryptedSize): ([UInt8], Int, UInt32) = try read( + from: inputStream, + password: password + ) + + // Write to the output + outputStream.write(chunk, maxLength: chunk.count) + remainingFileSize -= chunk.count + + // Update the progress + fileAmountProcessed += UInt64(chunkSizeBytesRead + chunk.count) + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) + } + + // Store the file path info and update the progress + switch isExtraFile { + case false: filePaths.append(fullPath) + case true: additionalFilePaths.append(fullPath) + } + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) + } + + // Validate that the number of files exported matches the number of paths we got back + let testEnumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( + at: URL(fileURLWithPath: destinationPath), + includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey] + ) + let tempFileUrls: [URL] = (testEnumerator?.allObjects + .compactMap { $0 as? URL } + .filter { url -> Bool in + guard + let resourceValues = try? url.resourceValues( + forKeys: [.isRegularFileKey, .isDirectoryKey] + ) + else { return true } + + return (resourceValues.isRegularFile == true) + }) + .defaulting(to: []) + + guard tempFileUrls.count == (filePaths.count + additionalFilePaths.count) else { + throw ArchiveError.importedFileCountMismatch + } + guard + filePaths.count == expectedFileCount && + additionalFilePaths.count == expectedAdditionalFileCount + else { throw ArchiveError.importedFileCountMetadataMismatch } + + return (filePaths, additionalFilePaths) + } + + private static func encrypt(buffer: [UInt8], password: String) throws -> [UInt8] { + guard let passwordData: Data = password.data(using: .utf8) else { + return buffer + } + + // Use HKDF for key derivation + let salt: Data = Data(count: 16) + let key: SymmetricKey = SymmetricKey(data: passwordData) + let symmetricKey: SymmetricKey = SymmetricKey( + data: HKDF.deriveKey( + inputKeyMaterial: key, + salt: salt, + outputByteCount: 32 + ) + ) + let nonce: AES.GCM.Nonce = AES.GCM.Nonce() + let sealedBox: AES.GCM.SealedBox = try AES.GCM.seal( + Data(buffer), + using: symmetricKey, + nonce: nonce + ) + + // Combine nonce, ciphertext, and tag + return [UInt8](nonce) + sealedBox.ciphertext + sealedBox.tag + } + + private static func decrypt(buffer: [UInt8], password: String) throws -> [UInt8] { + guard let passwordData: Data = password.data(using: .utf8) else { + return buffer + } + + let salt: Data = Data(count: 16) + let key: SymmetricKey = SymmetricKey(data: passwordData) + let symmetricKey: SymmetricKey = SymmetricKey( + data: HKDF.deriveKey( + inputKeyMaterial: key, + salt: salt, + outputByteCount: 32 + ) + ) + + // Extract nonce, ciphertext, and tag + let nonce: AES.GCM.Nonce = try AES.GCM.Nonce(data: Data(buffer.prefix(12))) + let ciphertext: Data = Data(buffer[12..<(buffer.count-16)]) + let tag: Data = Data(buffer.suffix(16)) + + // Decrypt with AES-GCM + let sealedBox: AES.GCM.SealedBox = try AES.GCM.SealedBox( + nonce: nonce, + ciphertext: ciphertext, + tag: tag + ) + + let decryptedData: Data = try AES.GCM.open(sealedBox, using: symmetricKey) + return [UInt8](decryptedData) + } + + private static func write( + _ data: [UInt8], + to outputStream: OutputStream, + blockSize: T.Type, + password: String? + ) throws where T: FixedWidthInteger, T: UnsignedInteger { + let processedBytes: [UInt8] + + switch password { + case .none: processedBytes = data + case .some(let password): + processedBytes = try encrypt( + buffer: data, + password: password + ) + } + + var blockSize: T = T(processedBytes.count) + let blockSizeData: [UInt8] = Array(Data(bytes: &blockSize, count: MemoryLayout.size)) + outputStream.write(blockSizeData, maxLength: blockSizeData.count) + outputStream.write(processedBytes, maxLength: processedBytes.count) + } + + private static func read( + from inputStream: InputStream, + password: String? + ) throws -> (value: [UInt8], blockSizeBytesRead: Int, encryptedSize: T) where T: FixedWidthInteger, T: UnsignedInteger { + var blockSizeBytes: [UInt8] = [UInt8](repeating: 0, count: MemoryLayout.size) + let bytesRead: Int = inputStream.read(&blockSizeBytes, maxLength: blockSizeBytes.count) + + switch bytesRead { + case 0: return ([], bytesRead, 0) // We have finished reading + case blockSizeBytes.count: break // We have started the next block + default: throw ArchiveError.unarchiveFailed // Invalid + } + + var blockSize: T = 0 + _ = withUnsafeMutableBytes(of: &blockSize) { blockSizeBuffer in + blockSizeBytes.copyBytes(to: blockSizeBuffer, from: ...size) + } + + var encryptedResult: [UInt8] = [UInt8](repeating: 0, count: Int(blockSize)) + guard inputStream.read(&encryptedResult, maxLength: encryptedResult.count) == encryptedResult.count else { + throw ArchiveError.unarchiveFailed + } + + let result: [UInt8] + switch password { + case .none: result = encryptedResult + case .some(let password): result = try decrypt(buffer: encryptedResult, password: password) + } + + return (result, bytesRead, blockSize) + } + + private static func exportFile( + sourcePath: String, + fileURL: URL, + customRelativePath: String?, + outputStream: OutputStream, + password: String?, + index: Int, + totalFiles: Int, + isExtraFile: Bool, + progressChanged: ((Int, Int, UInt64, UInt64) -> Void)? + ) throws { + guard + let values: URLResourceValues = try? fileURL.resourceValues( + forKeys: [.isRegularFileKey, .fileSizeKey] + ), + values.isRegularFile == true, + var fileSize: UInt64 = values.fileSize.map({ UInt64($0) }) + else { + progressChanged?(index, totalFiles, 1, 1) + return + } + + // Relative path preservation + let relativePath: String = customRelativePath + .defaulting( + to: fileURL.path + .replacingOccurrences(of: sourcePath, with: "") + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) + ) + + // Write path length and path + let pathData: Data = relativePath.data(using: .utf8)! + var pathLength: UInt32 = UInt32(pathData.count) + var isExtraFile: Bool = isExtraFile + + // Encrypt and write metadata (path length + path data) + let metadata: Data = ( + Data(bytes: &pathLength, count: MemoryLayout.size) + + pathData + + Data(bytes: &fileSize, count: MemoryLayout.size) + + Data(bytes: &isExtraFile, count: MemoryLayout.size) + ) + try write(Array(metadata), to: outputStream, blockSize: UInt64.self, password: password) + + // Stream file contents + guard let inputStream: InputStream = InputStream(url: fileURL) else { + progressChanged?(index, totalFiles, 1, 1) + return + } + + inputStream.open() + defer { inputStream.close() } + + var buffer: [UInt8] = [UInt8](repeating: 0, count: 4096) + var currentFileProcessAmount: UInt64 = 0 + while inputStream.hasBytesAvailable { + let bytesRead: Int = inputStream.read(&buffer, maxLength: buffer.count) + currentFileProcessAmount += UInt64(bytesRead) + progressChanged?(index, totalFiles, currentFileProcessAmount, fileSize) + + if bytesRead > 0 { + try write( + Array(buffer.prefix(bytesRead)), + to: outputStream, + blockSize: UInt32.self, + password: password + ) + } + } + } +} + +fileprivate extension InputStream { + func readEncryptedChunk(password: String, maxLength: Int) -> Data? { + var buffer: [UInt8] = [UInt8](repeating: 0, count: maxLength) + let bytesRead: Int = self.read(&buffer, maxLength: maxLength) + guard bytesRead > 0 else { return nil } + + return Data(buffer.prefix(bytesRead)) + } +} diff --git a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift index fe23a97d12d..54ba079c0df 100644 --- a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift +++ b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift @@ -31,6 +31,28 @@ public class ModalActivityIndicatorViewController: OWSViewController { return result }() + private lazy var stackView: UIStackView = { + let result: UIStackView = UIStackView() + result.axis = .vertical + result.spacing = Values.largeSpacing + result.alignment = .center + + return result + }() + + private lazy var messageLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.text = message + result.textAlignment = .center + result.themeTextColor = .textPrimary + result.lineBreakMode = .byWordWrapping + result.numberOfLines = 0 + result.isHidden = (message == nil) + + return result + }() + private lazy var spinner: NVActivityIndicatorView = { let result: NVActivityIndicatorView = NVActivityIndicatorView( frame: CGRect.zero, @@ -120,31 +142,15 @@ public class ModalActivityIndicatorViewController: OWSViewController { self.view.themeBackgroundColor = .clear self.view.addSubview(dimmingView) + self.view.addSubview(stackView) dimmingView.pin(to: self.view) + stackView.center(in: self.view) + + stackView.addArrangedSubview(spinner) + stackView.addArrangedSubview(messageLabel) + + messageLabel.set(.width, to: .width, of: stackView, withOffset: -(2 * Values.mediumSpacing)) - if let message = message { - let messageLabel: UILabel = UILabel() - messageLabel.font = .systemFont(ofSize: Values.mediumFontSize) - messageLabel.text = message - messageLabel.themeTextColor = .textPrimary - messageLabel.numberOfLines = 0 - messageLabel.textAlignment = .center - messageLabel.lineBreakMode = .byWordWrapping - messageLabel.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing) - - let stackView = UIStackView(arrangedSubviews: [ messageLabel, spinner ]) - stackView.axis = .vertical - stackView.spacing = Values.largeSpacing - stackView.alignment = .center - self.view.addSubview(stackView) - - stackView.center(in: self.view) - } - else { - self.view.addSubview(spinner) - spinner.center(in: self.view) - } - if canCancel { let cancelButton: SessionButton = SessionButton(style: .destructive, size: .large) cancelButton.setTitle("cancel".localized(), for: .normal) @@ -190,4 +196,9 @@ public class ModalActivityIndicatorViewController: OWSViewController { dismiss { } } + + public func setMessage(_ message: String?) { + messageLabel.text = message + messageLabel.isHidden = (message == nil) + } }