From fe14bb1b31ee2ceb45221c2c91cab4fc7219afc1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 13 Sep 2022 17:57:13 +1000 Subject: [PATCH] Fixed a couple of bugs and did some more theming Started trying to add the unit tests that were in the settings refactor branch (need to be refactored due to the implementation changes) Fixed a bug where the typing indicators wouldn't get cleaned up in some cases --- Podfile | 7 + Podfile.lock | 2 +- Session.xcodeproj/project.pbxproj | 297 ++++++- .../xcshareddata/xcschemes/Session.xcscheme | 26 + .../Views & Modals/IncomingCallBanner.swift | 120 ++- Session/Conversations/ConversationVC.swift | 8 +- .../ExpandingAttachmentsButton.swift | 21 +- .../Input View/InputTextView.swift | 43 +- .../Conversations/Input View/InputView.swift | 25 +- .../Input View/InputViewButton.swift | 112 ++- .../Views & Modals/ScrollToBottomButton.swift | 32 +- .../NotificationContentViewModel.swift | 14 +- .../Jobs/Types/GarbageCollectionJob.swift | 74 +- .../Open Groups/OpenGroupAPISpec.swift | 6 +- .../Open Groups/OpenGroupManagerSpec.swift | 21 +- .../_TestUtilities/MockGeneralCache.swift | 5 + ...eadDisappearingMessagesViewModelSpec.swift | 427 ++++++++++ .../ThreadSettingsViewModelSpec.swift | 767 ++++++++++++++++++ SessionTests/SessionTests.swift | 30 + .../NotificationContentViewModelSpec.swift | 157 ++++ .../Themes/Theme+ClassicDark.swift | 3 + .../Themes/Theme+ClassicLight.swift | 3 + .../Style Guide/Themes/Theme+OceanDark.swift | 7 +- .../Style Guide/Themes/Theme+OceanLight.swift | 3 + SessionUIKit/Style Guide/Themes/Theme.swift | 3 + 25 files changed, 2032 insertions(+), 181 deletions(-) create mode 100644 SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift create mode 100644 SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift create mode 100644 SessionTests/SessionTests.swift create mode 100644 SessionTests/Settings/NotificationContentViewModelSpec.swift diff --git a/Podfile b/Podfile index 1b0e98eae..7b30e8ef6 100644 --- a/Podfile +++ b/Podfile @@ -26,6 +26,13 @@ abstract_target 'GlobalDependencies' do pod 'YYImage/libwebp', git: 'https://github.com/signalapp/YYImage' pod 'ZXingObjC' pod 'DifferenceKit' + + target 'SessionTests' do + inherit! :complete + + pod 'Quick' + pod 'Nimble' + end end # Dependencies to be included only in all extensions/frameworks diff --git a/Podfile.lock b/Podfile.lock index db115496d..19757ea34 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -242,6 +242,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 6d640ed0e238b2eab2cf98ed1e125394b41041eb +PODFILE CHECKSUM: a646db9086664b8c9e5a0b3d17664a1275dcba9d COCOAPODS: 1.11.3 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 9b9626fb3..1167cdaef 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -543,6 +543,7 @@ D221A0E8169DFFC500537ABF /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D221A0E7169DFFC500537ABF /* AVFoundation.framework */; }; D24B5BD5169F568C00681372 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D24B5BD4169F568C00681372 /* AudioToolbox.framework */; }; D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */; }; + DA2AE22FA77136442EF669E9 /* Pods_GlobalDependencies_Session_SessionTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4C92F6ADBECCD47A6B6008E /* Pods_GlobalDependencies_Session_SessionTests.framework */; }; EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */ = {isa = PBXBuildFile; fileRef = EF764C341DB67CC5000D9A87 /* UIViewController+Permissions.m */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; @@ -714,6 +715,10 @@ FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115FF28C8253500B47552 /* UIView+Combine.swift */; }; FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71160128C8255900B47552 /* UIControl+Combine.swift */; }; FD71160428C95B5600B47552 /* PhotoCollectionPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71160328C95B5600B47552 /* PhotoCollectionPickerViewModel.swift */; }; + FD71160C28D00BAE00B47552 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71160B28D00BAE00B47552 /* SessionTests.swift */; }; + FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */; }; + FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */; }; + FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */; }; FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7162DA281B6C440060647B /* TypedTableAlias.swift */; }; FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */; }; FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */; }; @@ -980,6 +985,13 @@ remoteGlobalIDString = C3C2A678255388CC00C340D1; remoteInfo = SessionUtilitiesKit; }; + FD71160D28D00BAE00B47552 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = D221A088169C9E5E00537ABF; + remoteInfo = Session; + }; FD83B9B427CF200A005E1583 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D221A080169C9E5E00537ABF /* Project object */; @@ -1041,8 +1053,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 06160ECE3FE5A06A916FF8C5 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig"; sourceTree = ""; }; 0BF4561630A52BE96F164CF6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0E836037CC97CE5A47735596 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig"; sourceTree = ""; }; + 0E8564674E3201E218939AFB /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig"; sourceTree = ""; }; 18EAE958B8C12503F2C294DF /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-SessionUIKit/Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig"; sourceTree = ""; }; 1A0882BF820F5B44969F91F1 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; 245BF74EF6348E2D4125033F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig"; sourceTree = ""; }; @@ -1238,6 +1252,7 @@ A1C32D4F17A06537000A904E /* AddressBookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBookUI.framework; path = System/Library/Frameworks/AddressBookUI.framework; sourceTree = SDKROOT; }; A1FDCBEE16DAA6C300868894 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; A9C58C3ADF46C718488458C2 /* Pods_GlobalDependencies_SessionUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_SessionUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B4C92F6ADBECCD47A6B6008E /* Pods_GlobalDependencies_Session_SessionTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_Session_SessionTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B60EDE031A05A01700D73516 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; B646D10E1AA5461A004133BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; B657DDC91911A40500F45B0C /* Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Signal.entitlements; sourceTree = ""; }; @@ -1798,6 +1813,11 @@ FD7115FF28C8253500B47552 /* UIView+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Combine.swift"; sourceTree = ""; }; FD71160128C8255900B47552 /* UIControl+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Combine.swift"; sourceTree = ""; }; FD71160328C95B5600B47552 /* PhotoCollectionPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerViewModel.swift; sourceTree = ""; }; + FD71160928D00BAE00B47552 /* SessionTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FD71160B28D00BAE00B47552 /* SessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; + FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesViewModelSpec.swift; sourceTree = ""; }; + FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModelSpec.swift; sourceTree = ""; }; + FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModelSpec.swift; sourceTree = ""; }; FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = ""; }; FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManagerProtocol.swift; sourceTree = ""; }; FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentCallProtocol.swift; sourceTree = ""; }; @@ -2050,6 +2070,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FD71160628D00BAE00B47552 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DA2AE22FA77136442EF669E9 /* Pods_GlobalDependencies_Session_SessionTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FD83B9AC27CF200A005E1583 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2096,6 +2124,8 @@ 29CF8C79F41BF00B1C2E59A0 /* Pods-SessionUIKit.app store release.xcconfig */, 510955DC99A0FD84F2D1C159 /* Pods-GlobalDependencies-SessionUIKit.debug.xcconfig */, 18EAE958B8C12503F2C294DF /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */, + 06160ECE3FE5A06A916FF8C5 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */, + 0E8564674E3201E218939AFB /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -3426,12 +3456,13 @@ D221A093169C9E5E00537ABF /* Session */, 453518691FC635DD00210559 /* SessionShareExtension */, 7BC01A3C241F40AB00BC7C55 /* SessionNotificationServiceExtension */, - C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, C331FF1C2558F9D300070591 /* SessionUIKit */, C3C2A6F125539DE700C340D1 /* SessionMessagingKit */, C3C2A5A0255385C100C340D1 /* SessionSnodeKit */, C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */, + C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, FD83B9BC27CF2215005E1583 /* SharedTest */, + FD71160A28D00BAE00B47552 /* SessionTests */, FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */, FDE7214E287E50D50093DF33 /* Scripts */, @@ -3455,6 +3486,7 @@ C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */, FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */, FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */, + FD71160928D00BAE00B47552 /* SessionTests.xctest */, ); name = Products; sourceTree = ""; @@ -3507,6 +3539,7 @@ 6737124ECBC2DFEE2DD716D3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */, 48AD214D67ABED845101E795 /* Pods_GlobalDependencies_Session.framework */, A9C58C3ADF46C718488458C2 /* Pods_GlobalDependencies_SessionUIKit.framework */, + B4C92F6ADBECCD47A6B6008E /* Pods_GlobalDependencies_Session_SessionTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -3798,8 +3831,8 @@ FD3C906B27E43C2400CD579F /* Sending & Receiving */ = { isa = PBXGroup; children = ( - FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */, FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */, + FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */, ); path = "Sending & Receiving"; sourceTree = ""; @@ -3842,6 +3875,41 @@ path = "Disposable Views"; sourceTree = ""; }; + FD71160A28D00BAE00B47552 /* SessionTests */ = { + isa = PBXGroup; + children = ( + FD71161228D00D5300B47552 /* Conversations */, + FD71161828D00E0100B47552 /* Settings */, + FD71160B28D00BAE00B47552 /* SessionTests.swift */, + ); + path = SessionTests; + sourceTree = ""; + }; + FD71161228D00D5300B47552 /* Conversations */ = { + isa = PBXGroup; + children = ( + FD71161328D00D5D00B47552 /* Settings */, + ); + path = Conversations; + sourceTree = ""; + }; + FD71161328D00D5D00B47552 /* Settings */ = { + isa = PBXGroup; + children = ( + FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */, + FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */, + ); + path = Settings; + sourceTree = ""; + }; + FD71161828D00E0100B47552 /* Settings */ = { + isa = PBXGroup; + children = ( + FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */, + ); + path = Settings; + sourceTree = ""; + }; FD716E6F28505E5100C96BF4 /* Views */ = { isa = PBXGroup; children = ( @@ -3899,15 +3967,15 @@ FD83B9C127CF33EE005E1583 /* Models */ = { isa = PBXGroup; children = ( - FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */, FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */, FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */, + FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */, FDC2908627D7047F005DAE71 /* RoomSpec.swift */, FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */, - FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */, - FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */, FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */, + FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */, FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */, + FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */, ); path = Models; sourceTree = ""; @@ -3932,10 +4000,10 @@ FDC2909227D710A9005DAE71 /* Types */ = { isa = PBXGroup; children = ( + FDC2909927D71376005DAE71 /* NonceGeneratorSpec.swift */, + FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */, FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */, FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */, - FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */, - FDC2909927D71376005DAE71 /* NonceGeneratorSpec.swift */, FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */, ); path = Types; @@ -4016,8 +4084,8 @@ FDC4389B27BA01E300C60D73 /* _TestUtilities */, FD3C905D27E410DB00CD579F /* Common Networking */, FD3C906527E416A200CD579F /* Contacts */, - FD3C906B27E43C2400CD579F /* Sending & Receiving */, FDC4389827BA001800C60D73 /* Open Groups */, + FD3C906B27E43C2400CD579F /* Sending & Receiving */, FD3C906827E417B100CD579F /* Utilities */, ); path = SessionMessagingKitTests; @@ -4375,6 +4443,26 @@ productReference = D221A089169C9E5E00537ABF /* Session.app */; productType = "com.apple.product-type.application"; }; + FD71160828D00BAE00B47552 /* SessionTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FD71160F28D00BAE00B47552 /* Build configuration list for PBXNativeTarget "SessionTests" */; + buildPhases = ( + 567FCF8CB93B411EE1FD4BBF /* [CP] Check Pods Manifest.lock */, + FD71160528D00BAE00B47552 /* Sources */, + FD71160628D00BAE00B47552 /* Frameworks */, + FD71160728D00BAE00B47552 /* Resources */, + A08B0675BD19884F61FF48D9 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + FD71160E28D00BAE00B47552 /* PBXTargetDependency */, + ); + name = SessionTests; + productName = SessionTests; + productReference = FD71160928D00BAE00B47552 /* SessionTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */ = { isa = PBXNativeTarget; buildConfigurationList = FD83B9B627CF200A005E1583 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKitTests" */; @@ -4424,7 +4512,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastSwiftUpdateCheck = 1320; + LastSwiftUpdateCheck = 1340; LastTestingUpgradeCheck = 0600; LastUpgradeCheck = 1320; ORGANIZATIONNAME = "Rangeproof Pty Ltd"; @@ -4514,6 +4602,10 @@ }; }; }; + FD71160828D00BAE00B47552 = { + CreatedOnToolsVersion = 13.4.1; + TestTargetID = D221A088169C9E5E00537ABF; + }; FD83B9AE27CF200A005E1583 = { CreatedOnToolsVersion = 13.2.1; }; @@ -4564,6 +4656,7 @@ C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */, C3C2A59E255385C100C340D1 /* SessionSnodeKit */, C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */, + FD71160828D00BAE00B47552 /* SessionTests */, FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */, FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */, ); @@ -4691,6 +4784,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FD71160728D00BAE00B47552 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FD83B9AD27CF200A005E1583 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4813,6 +4913,28 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 567FCF8CB93B411EE1FD4BBF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-Session-SessionTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 6E75B456D9C7705F6FD9C9D4 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -4852,6 +4974,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + A08B0675BD19884F61FF48D9 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; AF68D547A722E10BF230F662 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -5686,6 +5825,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FD71160528D00BAE00B47552 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */, + FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */, + FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */, + FD71160C28D00BAE00B47552 /* SessionTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FD83B9AB27CF200A005E1583 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -5842,6 +5992,11 @@ target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */; targetProxy = FD37E9F128A5ED70003AE748 /* PBXContainerItemProxy */; }; + FD71160E28D00BAE00B47552 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D221A088169C9E5E00537ABF /* Session */; + targetProxy = FD71160D28D00BAE00B47552 /* PBXContainerItemProxy */; + }; FD83B9B527CF200A005E1583 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */; @@ -7206,6 +7361,113 @@ }; name = "App Store Release"; }; + FD71161028D00BAE00B47552 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 06160ECE3FE5A06A916FF8C5 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/Session"; + }; + name = Debug; + }; + FD71161128D00BAE00B47552 /* App Store Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E8564674E3201E218939AFB /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/Session"; + VALIDATE_PRODUCT = YES; + }; + name = "App Store Release"; + }; FD83B9B727CF200A005E1583 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = BE11AFA6FD8CAE894CABC28D /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig */; @@ -7233,7 +7495,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -7296,7 +7558,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -7339,7 +7601,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -7402,7 +7664,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -7502,6 +7764,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = "App Store Release"; }; + FD71160F28D00BAE00B47552 /* Build configuration list for PBXNativeTarget "SessionTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FD71161028D00BAE00B47552 /* Debug */, + FD71161128D00BAE00B47552 /* App Store Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "App Store Release"; + }; FD83B9B627CF200A005E1583 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index 82d7881ad..ba9deefc8 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:Session.xcodeproj"> + + + + + + + + Bool { if gestureRecognizer == panGestureRecognizer { let v = panGestureRecognizer.velocity(in: self) + return abs(v.y) > abs(v.x) // It has to be more vertical than horizontal - } else { - return true } + + return true } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { @@ -125,20 +159,27 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { let translationY = gestureRecognizer.translation(in: self).y switch gestureRecognizer.state { - case .changed: - self.transform = CGAffineTransform(translationX: 0, y: min(translationY, IncomingCallBanner.swipeToOperateThreshold)) - if abs(translationY) > IncomingCallBanner.swipeToOperateThreshold && abs(previousY) < IncomingCallBanner.swipeToOperateThreshold { - UIImpactFeedbackGenerator(style: .heavy).impactOccurred() // Let the user know when they've hit the swipe to reply threshold - } - previousY = translationY - case .ended, .cancelled: - if abs(translationY) > IncomingCallBanner.swipeToOperateThreshold { - if translationY > 0 { showCallVC(answer: false) } - else { endCall() } // TODO: Or just put the call on hold? - } else { - self.transform = .identity - } - default: break + case .changed: + self.transform = CGAffineTransform(translationX: 0, y: min(translationY, IncomingCallBanner.swipeToOperateThreshold)) + if abs(translationY) > IncomingCallBanner.swipeToOperateThreshold && abs(previousY) < IncomingCallBanner.swipeToOperateThreshold { + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() // Let the user know when they've hit the swipe to reply threshold + } + previousY = translationY + + case .ended, .cancelled: + if abs(translationY) > IncomingCallBanner.swipeToOperateThreshold { + if translationY > 0 { + showCallVC(answer: false) + } + else { + endCall() // TODO: Or just put the call on hold? + } + } + else { + self.transform = .identity + } + + default: break } } @@ -152,6 +193,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { self.call.endSessionCall() AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) } + self.dismiss() } } @@ -159,14 +201,18 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { public func showCallVC(answer: Bool) { dismiss() guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // FIXME: Handle more gracefully + let callVC = CallVC(for: self.call) if let conversationVC = presentingVC as? ConversationVC { callVC.conversationVC = conversationVC conversationVC.inputAccessoryView?.isHidden = true conversationVC.inputAccessoryView?.alpha = 0 } - presentingVC.present(callVC, animated: true) { - if answer { self.call.answerSessionCall() } + + presentingVC.present(callVC, animated: true) { [weak self] in + guard answer else { return } + + self?.call.answerSessionCall() } } @@ -174,12 +220,15 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { self.alpha = 0.0 let window = CurrentAppContext().mainWindow! window.addSubview(self) + let topMargin = window.safeAreaInsets.top - Values.smallSpacing self.autoPinWidthToSuperview(withMargin: Values.smallSpacing) self.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) + UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { self.alpha = 1.0 }, completion: nil) + CallRingTonePlayer.shared.startVibration() CallRingTonePlayer.shared.startPlayingRingTone() } @@ -187,6 +236,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { public func dismiss() { CallRingTonePlayer.shared.stopVibrationIfPossible() CallRingTonePlayer.shared.stopPlayingRingTone() + UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { self.alpha = 0.0 }, completion: { _ in diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e165c03b0..e39dcffce 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -165,11 +165,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl lazy var unreadCountView: UIView = { let result: UIView = UIView() - result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity) - result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize) - result.set(.height, to: ConversationVC.unreadCountViewSize) + result.themeBackgroundColor = .backgroundSecondary result.layer.masksToBounds = true result.layer.cornerRadius = (ConversationVC.unreadCountViewSize / 2) + result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize) + result.set(.height, to: ConversationVC.unreadCountViewSize) return result }() @@ -177,7 +177,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl lazy var unreadCountLabel: UILabel = { let result: UILabel = UILabel() result.font = .boldSystemFont(ofSize: Values.verySmallFontSize) - result.textColor = Colors.text + result.themeTextColor = .textPrimary result.textAlignment = .center return result diff --git a/Session/Conversations/Input View/ExpandingAttachmentsButton.swift b/Session/Conversations/Input View/ExpandingAttachmentsButton.swift index 9fbad5a1d..f2e983a03 100644 --- a/Session/Conversations/Input View/ExpandingAttachmentsButton.swift +++ b/Session/Conversations/Input View/ExpandingAttachmentsButton.swift @@ -1,5 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate { +import UIKit +import SessionUIKit + +final class ExpandingAttachmentsButton: UIView, InputViewButtonDelegate { private weak var delegate: ExpandingAttachmentsButtonDelegate? private var isExpanded = false { didSet { expandOrCollapse() } } @@ -22,31 +26,36 @@ final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate { // MARK: UI Components lazy var gifButton: InputViewButton = { let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_gif_black"), delegate: self, hasOpaqueBackground: true) - result.accessibilityLabel = NSLocalizedString("accessibility_gif_button", comment: "") + result.accessibilityLabel = "accessibility_gif_button".localized() + return result }() lazy var gifButtonContainer = container(for: gifButton) lazy var documentButton: InputViewButton = { let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_document_black"), delegate: self, hasOpaqueBackground: true) - result.accessibilityLabel = NSLocalizedString("accessibility_document_button", comment: "") + result.accessibilityLabel = "accessibility_document_button".localized() + return result }() lazy var documentButtonContainer = container(for: documentButton) lazy var libraryButton: InputViewButton = { let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_roll_black"), delegate: self, hasOpaqueBackground: true) - result.accessibilityLabel = NSLocalizedString("accessibility_library_button", comment: "") + result.accessibilityLabel = "accessibility_library_button".localized() + return result }() lazy var libraryButtonContainer = container(for: libraryButton) lazy var cameraButton: InputViewButton = { let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_black"), delegate: self, hasOpaqueBackground: true) - result.accessibilityLabel = NSLocalizedString("accessibility_camera_button", comment: "") + result.accessibilityLabel = "accessibility_camera_button".localized() + return result }() lazy var cameraButtonContainer = container(for: cameraButton) lazy var mainButton: InputViewButton = { let result = InputViewButton(icon: #imageLiteral(resourceName: "ic_plus_24"), delegate: self) - result.accessibilityLabel = NSLocalizedString("accessibility_expanding_attachments_button", comment: "") + result.accessibilityLabel = "accessibility_expanding_attachments_button".localized() + return result }() lazy var mainButtonContainer = container(for: mainButton) diff --git a/Session/Conversations/Input View/InputTextView.swift b/Session/Conversations/Input View/InputTextView.swift index 13d7cb3aa..aaaf0e3f0 100644 --- a/Session/Conversations/Input View/InputTextView.swift +++ b/Session/Conversations/Input View/InputTextView.swift @@ -1,33 +1,43 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -public final class InputTextView : UITextView, UITextViewDelegate { +import UIKit +import SessionUIKit + +public final class InputTextView: UITextView, UITextViewDelegate { private weak var snDelegate: InputTextViewDelegate? private let maxWidth: CGFloat private lazy var heightConstraint = self.set(.height, to: minHeight) public override var text: String? { didSet { handleTextChanged() } } - // MARK: UI Components + // MARK: - UI Components + private lazy var placeholderLabel: UILabel = { let result = UILabel() - result.text = NSLocalizedString("vc_conversation_input_prompt", comment: "") result.font = .systemFont(ofSize: Values.mediumFontSize) - result.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity) + result.text = "vc_conversation_input_prompt".localized() + result.themeTextColor = .textSecondary + return result }() - // MARK: Settings + // MARK: - Settings + private let minHeight: CGFloat = 22 private let maxHeight: CGFloat = 80 - // MARK: Lifecycle + // MARK: - Lifecycle + init(delegate: InputTextViewDelegate, maxWidth: CGFloat) { snDelegate = delegate self.maxWidth = maxWidth + super.init(frame: CGRect.zero, textContainer: nil) + setUpViewHierarchy() self.delegate = self self.isAccessibilityElement = true - self.accessibilityLabel = NSLocalizedString("vc_conversation_input_prompt", comment: "") + self.accessibilityLabel = "vc_conversation_input_prompt".localized() } public override init(frame: CGRect, textContainer: NSTextContainer?) { @@ -57,11 +67,12 @@ public final class InputTextView : UITextView, UITextViewDelegate { private func setUpViewHierarchy() { showsHorizontalScrollIndicator = false showsVerticalScrollIndicator = false - backgroundColor = .clear - textColor = Colors.text + font = .systemFont(ofSize: Values.mediumFontSize) - tintColor = Colors.accent - keyboardAppearance = isLightMode ? .light : .dark + themeBackgroundColor = .clear + themeTextColor = .textPrimary + themeTintColor = .primary + heightConstraint.isActive = true let horizontalInset: CGFloat = 2 textContainerInset = UIEdgeInsets(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset) @@ -70,9 +81,17 @@ public final class InputTextView : UITextView, UITextViewDelegate { placeholderLabel.pin(.top, to: .top, of: self) pin(.trailing, to: .trailing, of: placeholderLabel, withInset: horizontalInset) pin(.bottom, to: .bottom, of: placeholderLabel) + + ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in + switch theme.interfaceStyle { + case .light: self?.keyboardAppearance = .light + default: self?.keyboardAppearance = .dark + } + } } - // MARK: Updating + // MARK: - Updating + public func textViewDidChange(_ textView: UITextView) { handleTextChanged() } diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 2bbc8d332..bd84e410a 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -54,15 +54,17 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M private lazy var voiceMessageButton: InputViewButton = { let result = InputViewButton(icon: #imageLiteral(resourceName: "Microphone"), delegate: self) - result.accessibilityLabel = NSLocalizedString("VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE", comment: "") - result.accessibilityHint = NSLocalizedString("VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE", comment: "") + result.accessibilityLabel = "VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE".localized() + result.accessibilityHint = "VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized() + return result }() private lazy var sendButton: InputViewButton = { let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self) result.isHidden = true - result.accessibilityLabel = NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON", comment: "") + result.accessibilityLabel = "ATTACHMENT_APPROVAL_SEND_BUTTON".localized() + return result }() private lazy var voiceMessageButtonContainer = container(for: voiceMessageButton) @@ -77,7 +79,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M private lazy var mentionsViewContainer: UIView = { let result: UIView = UIView() let backgroundView = UIView() - backgroundView.backgroundColor = (isLightMode ? .white : .black) + backgroundView.themeBackgroundColor = .backgroundSecondary backgroundView.alpha = Values.lowOpacity result.addSubview(backgroundView) backgroundView.pin(to: result) @@ -103,8 +105,8 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M private lazy var disabledInputLabel: UILabel = { let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: Values.smallFontSize) - label.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity) + label.font = .systemFont(ofSize: Values.smallFontSize) + label.themeTextColor = .textPrimary label.textAlignment = .center label.alpha = 0 @@ -137,7 +139,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // Background & blur let backgroundView = UIView() - backgroundView.backgroundColor = isLightMode ? .white : .black + backgroundView.themeBackgroundColor = .backgroundSecondary backgroundView.alpha = Values.lowOpacity addSubview(backgroundView) backgroundView.pin(to: self) @@ -148,8 +150,8 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // Separator let separator = UIView() - separator.backgroundColor = Colors.text.withAlphaComponent(0.2) - separator.set(.height, to: 1 / UIScreen.main.scale) + separator.themeBackgroundColor = .borderSeparator + separator.set(.height, to: Values.separatorThickness) addSubview(separator) separator.pin([ UIView.HorizontalEdge.leading, UIView.VerticalEdge.top, UIView.HorizontalEdge.trailing ], to: self) @@ -330,7 +332,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M 1 : (messageTypes == .textOnly ? 0.4 : 0) ) - self?.disabledInputLabel.alpha = (messageTypes != .none ? 0 : 1) + self?.disabledInputLabel.alpha = (messageTypes != .none ? 0 : Values.mediumOpacity) } } @@ -371,8 +373,9 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M if inputViewButton == sendButton { delegate?.handleSendButtonTapped() } } - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton) { + func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { guard inputViewButton == voiceMessageButton else { return } + delegate?.startVoiceMessageRecording() showVoiceMessageUI() } diff --git a/Session/Conversations/Input View/InputViewButton.swift b/Session/Conversations/Input View/InputViewButton.swift index a9d2ca015..ca42e1943 100644 --- a/Session/Conversations/Input View/InputViewButton.swift +++ b/Session/Conversations/Input View/InputViewButton.swift @@ -1,5 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -final class InputViewButton : UIView { +import UIKit +import SessionUIKit + +final class InputViewButton: UIView { private let icon: UIImage private let isSendButton: Bool private weak var delegate: InputViewButtonDelegate? @@ -9,21 +13,27 @@ final class InputViewButton : UIView { private var longPressTimer: Timer? private var isLongPress = false - // MARK: UI Components - private lazy var backgroundView = UIView() + // MARK: - UI Components - // MARK: Settings - static let size = CGFloat(40) - static let expandedSize = CGFloat(48) + private lazy var backgroundView: UIView = UIView() + private lazy var iconImageView: UIImageView = UIImageView() + + // MARK: - Settings + + static let size: CGFloat = 40 + static let expandedSize: CGFloat = 48 static let iconSize: CGFloat = 20 - // MARK: Lifecycle + // MARK: - Lifecycle + init(icon: UIImage, isSendButton: Bool = false, delegate: InputViewButtonDelegate, hasOpaqueBackground: Bool = false) { self.icon = icon self.isSendButton = isSendButton self.delegate = delegate self.hasOpaqueBackground = hasOpaqueBackground + super.init(frame: CGRect.zero) + setUpViewHierarchy() self.isAccessibilityElement = true } @@ -37,63 +47,84 @@ final class InputViewButton : UIView { } private func setUpViewHierarchy() { - backgroundColor = .clear + themeBackgroundColor = .clear + if hasOpaqueBackground { - let backgroundView = UIView() - backgroundView.backgroundColor = isLightMode ? .white : .black + let backgroundView: UIView = UIView() + backgroundView.themeBackgroundColor = .inputButton_background backgroundView.alpha = Values.lowOpacity addSubview(backgroundView) backgroundView.pin(to: self) - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + + let blurView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) addSubview(blurView) blurView.pin(to: self) + + themeBorderColor = .borderSeparator layer.borderWidth = Values.separatorThickness - let borderColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.veryLowOpacity) - layer.borderColor = borderColor.cgColor } - backgroundView.backgroundColor = isSendButton ? Colors.accent : Colors.text.withAlphaComponent(0.05) + + backgroundView.themeBackgroundColor = (isSendButton ? .primary : .inputButton_background) + backgroundView.alpha = (isSendButton ? 1 : Values.lowOpacity) addSubview(backgroundView) backgroundView.pin(to: self) - layer.cornerRadius = InputViewButton.size / 2 + + layer.cornerRadius = (InputViewButton.size / 2) layer.masksToBounds = true isUserInteractionEnabled = true widthConstraint.isActive = true heightConstraint.isActive = true - let iconImageView = UIImageView(image: icon.withRenderingMode(.alwaysTemplate)) - iconImageView.tintColor = (isSendButton ? UIColor.black : Colors.text) + + iconImageView.image = icon.withRenderingMode(.alwaysTemplate) + iconImageView.themeTintColor = (isSendButton ? .black : .textPrimary) iconImageView.contentMode = .scaleAspectFit - let iconSize = InputViewButton.iconSize - iconImageView.set(.width, to: iconSize) - iconImageView.set(.height, to: iconSize) addSubview(iconImageView) iconImageView.center(in: self) + iconImageView.set(.width, to: InputViewButton.iconSize) + iconImageView.set(.height, to: InputViewButton.iconSize) } - // MARK: Animation - private func animate(to size: CGFloat, glowColor: UIColor, backgroundColor: UIColor) { + // MARK: - Animation + + private func animate( + to size: CGFloat, + themeBackgroundColor: ThemeValue, + themeTintColor: ThemeValue, + alpha: CGFloat + ) { let frame = CGRect(center: center, size: CGSize(width: size, height: size)) widthConstraint.constant = size heightConstraint.constant = size + UIView.animate(withDuration: 0.25) { self.layoutIfNeeded() self.frame = frame - self.layer.cornerRadius = size / 2 - let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6) - self.setCircularGlow(with: glowConfiguration) - self.backgroundView.backgroundColor = backgroundColor + self.layer.cornerRadius = (size / 2) + self.iconImageView.themeTintColor = themeTintColor + self.backgroundView.themeBackgroundColor = themeBackgroundColor + self.backgroundView.alpha = alpha } } private func expand() { - animate(to: InputViewButton.expandedSize, glowColor: Colors.expandedButtonGlowColor, backgroundColor: Colors.accent) + animate( + to: InputViewButton.expandedSize, + themeBackgroundColor: .primary, + themeTintColor: .black, + alpha: 1 + ) } private func collapse() { - let backgroundColor = isSendButton ? Colors.accent : Colors.text.withAlphaComponent(0.05) - animate(to: InputViewButton.size, glowColor: .clear, backgroundColor: backgroundColor) + animate( + to: InputViewButton.size, + themeBackgroundColor: (isSendButton ? .primary : .inputButton_background), + themeTintColor: (isSendButton ? .black : .textPrimary), + alpha: (isSendButton ? 1 : Values.lowOpacity) + ) } - // MARK: Interaction + // MARK: - Interaction // We want to detect both taps and long presses @@ -104,9 +135,8 @@ final class InputViewButton : UIView { expand() invalidateLongPressIfNeeded() longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in - guard let self = self else { return } - self.isLongPress = true - self.delegate?.handleInputViewButtonLongPressBegan(self) + self?.isLongPress = true + self?.delegate?.handleInputViewButtonLongPressBegan(self) }) } @@ -114,7 +144,7 @@ final class InputViewButton : UIView { guard isUserInteractionEnabled else { return } if isLongPress { - delegate?.handleInputViewButtonLongPressMoved(self, with: touches.first!) + delegate?.handleInputViewButtonLongPressMoved(self, with: touches.first) } } @@ -125,7 +155,7 @@ final class InputViewButton : UIView { if !isLongPress { delegate?.handleInputViewButtonTapped(self) } else { - delegate?.handleInputViewButtonLongPressEnded(self, with: touches.first!) + delegate?.handleInputViewButtonLongPressEnded(self, with: touches.first) } invalidateLongPressIfNeeded() } @@ -145,13 +175,13 @@ final class InputViewButton : UIView { protocol InputViewButtonDelegate: AnyObject { func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton) - func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch) - func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch) + func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) + func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) + func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) } extension InputViewButtonDelegate { - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton) { } - func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch) { } - func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch) { } + func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { } + func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { } + func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { } } diff --git a/Session/Conversations/Views & Modals/ScrollToBottomButton.swift b/Session/Conversations/Views & Modals/ScrollToBottomButton.swift index 871f51c64..7a3bf9793 100644 --- a/Session/Conversations/Views & Modals/ScrollToBottomButton.swift +++ b/Session/Conversations/Views & Modals/ScrollToBottomButton.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import SessionUIKit final class ScrollToBottomButton: UIView { private weak var delegate: ScrollToBottomButtonDelegate? @@ -14,7 +15,9 @@ final class ScrollToBottomButton: UIView { init(delegate: ScrollToBottomButtonDelegate) { self.delegate = delegate + super.init(frame: CGRect.zero) + setUpViewHierarchy() } @@ -29,32 +32,37 @@ final class ScrollToBottomButton: UIView { private func setUpViewHierarchy() { // Background & blur let backgroundView = UIView() - backgroundView.backgroundColor = isLightMode ? .white : .black + backgroundView.themeBackgroundColor = .backgroundSecondary backgroundView.alpha = Values.lowOpacity addSubview(backgroundView) backgroundView.pin(to: self) + let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) addSubview(blurView) blurView.pin(to: self) + // Size & shape - let size = ScrollToBottomButton.size - set(.width, to: size) - set(.height, to: size) - layer.cornerRadius = size / 2 + set(.width, to: ScrollToBottomButton.size) + set(.height, to: ScrollToBottomButton.size) + layer.cornerRadius = (ScrollToBottomButton.size / 2) layer.masksToBounds = true + // Border + self.themeBorderColor = .borderSeparator layer.borderWidth = Values.separatorThickness - let borderColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.veryLowOpacity) - layer.borderColor = borderColor.cgColor + // Icon - let tint = isLightMode ? UIColor.black : UIColor.white - let icon = UIImage(named: "ic_chevron_down")!.withTint(tint) - let iconImageView = UIImageView(image: icon) - iconImageView.set(.width, to: ScrollToBottomButton.iconSize) - iconImageView.set(.height, to: ScrollToBottomButton.iconSize) + let iconImageView = UIImageView( + image: UIImage(named: "ic_chevron_down")? + .withRenderingMode(.alwaysTemplate) + ) + iconImageView.themeTintColor = .textPrimary iconImageView.contentMode = .scaleAspectFit addSubview(iconImageView) iconImageView.center(in: self) + iconImageView.set(.width, to: ScrollToBottomButton.iconSize) + iconImageView.set(.height, to: ScrollToBottomButton.iconSize) + // Gesture recognizer let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) addGestureRecognizer(tapGestureRecognizer) diff --git a/Session/Settings/NotificationContentViewModel.swift b/Session/Settings/NotificationContentViewModel.swift index f02d85092..c75a9126e 100644 --- a/Session/Settings/NotificationContentViewModel.swift +++ b/Session/Settings/NotificationContentViewModel.swift @@ -8,6 +8,14 @@ import SessionMessagingKit import SessionUtilitiesKit class NotificationContentViewModel: SettingsTableViewModel { + private let storage: Storage + + // MARK: - Initialization + + init(storage: Storage = Storage.shared) { + self.storage = storage + } + // MARK: - Section public enum Section: SettingSection { @@ -31,7 +39,7 @@ class NotificationContentViewModel: SettingsTableViewModel [SectionModel] in + .trackingConstantRegion { [storage] db -> [SectionModel] in let currentSelection: Preferences.NotificationPreviewType? = db[.preferencesNotificationPreviewType] .defaulting(to: .defaultPreviewType) @@ -48,7 +56,7 @@ class NotificationContentViewModel: SettingsTableViewModel (23 * 60 * 60) - else { - deferred(job) - return - } + let finalTypesToCollection: Set = { + guard + job.behaviour != .recurringOnActive || + Date().timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60) + else { + // Note: This should only contain the `Types` which are unlikely to ever cause + // a startup delay (ie. avoid mass deletions and file management) + return typesToCollect.asSet() + .intersection([ + .threadTypingIndicators + ]) + } + + return typesToCollect.asSet() + }() Storage.shared.writeAsync( updates: { db in + /// Remove any typing indicators + if finalTypesToCollection.contains(.threadTypingIndicators) { + _ = try ThreadTypingIndicator + .deleteAll(db) + } + /// Remove any expired controlMessageProcessRecords - if typesToCollect.contains(.expiredControlMessageProcessRecords) { + if finalTypesToCollection.contains(.expiredControlMessageProcessRecords) { _ = try ControlMessageProcessRecord .filter(ControlMessageProcessRecord.Columns.serverExpirationTimestamp <= timestampNow) .deleteAll(db) } - /// Remove any typing indicators - if typesToCollect.contains(.threadTypingIndicators) { - _ = try ThreadTypingIndicator - .deleteAll(db) - } - /// Remove any old open group messages - open group messages which are older than six months - if typesToCollect.contains(.oldOpenGroupMessages) && db[.trimOpenGroupMessagesOlderThanSixMonths] { + if finalTypesToCollection.contains(.oldOpenGroupMessages) && db[.trimOpenGroupMessagesOlderThanSixMonths] { let interaction: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name) @@ -97,7 +104,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned jobs - jobs which have had their threads or interactions removed - if typesToCollect.contains(.orphanedJobs) { + if finalTypesToCollection.contains(.orphanedJobs) { let job: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() @@ -123,7 +130,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned link previews - link previews which have no interactions with matching url & rounded timestamps - if typesToCollect.contains(.orphanedLinkPreviews) { + if finalTypesToCollection.contains(.orphanedLinkPreviews) { let linkPreview: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() @@ -143,7 +150,7 @@ public enum GarbageCollectionJob: JobExecutor { /// Orphaned open groups - open groups which are no longer associated to a thread (except for the session-run ones for which /// we want cached image data even if the user isn't in the group) - if typesToCollect.contains(.orphanedOpenGroups) { + if finalTypesToCollection.contains(.orphanedOpenGroups) { let openGroup: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() @@ -162,7 +169,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned open group capabilities - capabilities which have no existing open groups with the same server - if typesToCollect.contains(.orphanedOpenGroupCapabilities) { + if finalTypesToCollection.contains(.orphanedOpenGroupCapabilities) { let capability: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() @@ -178,7 +185,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned blinded id lookups - lookups which have no existing threads or approval/block settings for either blinded/un-blinded id - if typesToCollect.contains(.orphanedBlindedIdLookups) { + if finalTypesToCollection.contains(.orphanedBlindedIdLookups) { let blindedIdLookup: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() @@ -206,7 +213,7 @@ public enum GarbageCollectionJob: JobExecutor { /// Approved blinded contact records - once a blinded contact has been approved there is no need to keep the blinded /// contact record around anymore - if typesToCollect.contains(.approvedBlindedContactRecords) { + if finalTypesToCollection.contains(.approvedBlindedContactRecords) { let contact: TypedTableAlias = TypedTableAlias() let blindedIdLookup: TypedTableAlias = TypedTableAlias() @@ -225,7 +232,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned attachments - attachments which have no related interactions, quotes or link previews - if typesToCollect.contains(.orphanedAttachments) { + if finalTypesToCollection.contains(.orphanedAttachments) { let attachment: TypedTableAlias = TypedTableAlias() let quote: TypedTableAlias = TypedTableAlias() let linkPreview: TypedTableAlias = TypedTableAlias() @@ -248,7 +255,7 @@ public enum GarbageCollectionJob: JobExecutor { """) } - if typesToCollect.contains(.orphanedProfiles) { + if finalTypesToCollection.contains(.orphanedProfiles) { let profile: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() @@ -297,7 +304,7 @@ public enum GarbageCollectionJob: JobExecutor { var profileAvatarFilenames: Set = [] /// Orphaned attachment files - attachment files which don't have an associated record in the database - if typesToCollect.contains(.orphanedAttachmentFiles) { + if finalTypesToCollection.contains(.orphanedAttachmentFiles) { /// **Note:** Thumbnails are stored in the `NSCachesDirectory` directory which should be automatically manage /// it's own garbage collection so we can just ignore it according to the various comments in the following stack overflow /// post, the directory will be cleared during app updates as well as if the system is running low on memory (if the app isn't running) @@ -310,7 +317,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned profile avatar files - profile avatar files which don't have an associated record in the database - if typesToCollect.contains(.orphanedProfileAvatars) { + if finalTypesToCollection.contains(.orphanedProfileAvatars) { profileAvatarFilenames = try Profile .select(.profilePictureFileName) .filter(Profile.Columns.profilePictureFileName != nil) @@ -333,7 +340,7 @@ public enum GarbageCollectionJob: JobExecutor { var deletionErrors: [Error] = [] // Orphaned attachment files (actual deletion) - if typesToCollect.contains(.orphanedAttachmentFiles) { + if finalTypesToCollection.contains(.orphanedAttachmentFiles) { // Note: Looks like in order to recursively look through files we need to use the // enumerator method let fileEnumerator = FileManager.default.enumerator( @@ -377,7 +384,7 @@ public enum GarbageCollectionJob: JobExecutor { } // Orphaned profile avatar files (actual deletion) - if typesToCollect.contains(.orphanedProfileAvatars) { + if finalTypesToCollection.contains(.orphanedProfileAvatars) { let allAvatarProfileFilenames: Set = (try? FileManager.default .contentsOfDirectory(atPath: ProfileManager.sharedDataProfileAvatarsDirPath)) .defaulting(to: []) @@ -405,9 +412,12 @@ public enum GarbageCollectionJob: JobExecutor { return } - // Update the 'lastGarbageCollection' date to prevent this job from running again - // for the next 23 hours - UserDefaults.standard[.lastGarbageCollection] = Date() + // If we did a full collection then update the 'lastGarbageCollection' date to + // prevent a full collection from running again in the next 23 hours + if job.behaviour == .recurringOnActive && Date().timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60) { + UserDefaults.standard[.lastGarbageCollection] = Date() + } + success(job, false) } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index e4edc8803..3af36df80 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -1243,7 +1243,8 @@ class OpenGroupAPISpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } @@ -1612,7 +1613,8 @@ class OpenGroupAPISpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 2158aa3e6..48598cd03 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -204,7 +204,8 @@ class OpenGroupManagerSpec: QuickSpec { "AAAAAAAAAAAAAAAAAAAAA", "AA" ].joined(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) testDirectMessage = OpenGroupAPI.DirectMessage( id: 128, @@ -2115,7 +2116,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2175,7 +2177,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2207,7 +2210,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2249,7 +2253,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ), testMessage, ], @@ -2287,7 +2292,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2315,7 +2321,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", diff --git a/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift b/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift index c47b8d6eb..0d3c55b78 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift @@ -10,4 +10,9 @@ class MockGeneralCache: Mock, GeneralCacheType { get { return accept() as? String } set { accept(args: [newValue]) } } + + var recentReactionTimestamps: [Int64] { + get { return (accept() as? [Int64] ?? []) } + set { accept(args: [newValue]) } + } } diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift new file mode 100644 index 000000000..a8edcd510 --- /dev/null +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -0,0 +1,427 @@ +//// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +//import Combine +//import Quick +//import Nimble +// +//@testable import Session +// +//class ThreadDisappearingMessagesViewModelSpec: import Combine { +// typealias Item = ConversationDisappearingMessagesViewModel.Item +// +// var disposables: Set! +// var dataChangedCallbackTriggered: Bool = false +// var thread: TSThread! +// var config: OWSDisappearingMessagesConfiguration! +// var contact: Contact! +// var defaultItems: [ConversationDisappearingMessagesViewModel.Item]! +// var defaultItems: [Item]! +// var viewModel: ConversationDisappearingMessagesViewModel! +// +// // MARK: - Configuration +// +// override func setUpWithError() throws { +// dataChangedCallbackTriggered = false +// +// disposables = Set() +// thread = TSContactThread(uniqueId: "TestId") +// config = OWSDisappearingMessagesConfiguration(defaultWithThreadId: "TestId") +// contact = Contact(sessionID: "TestContactId") +// defaultItems = [ +// ConversationDisappearingMessagesViewModel.Item(id: 0, title: "Off", isActive: true), +// ConversationDisappearingMessagesViewModel.Item(id: 1, title: "5 seconds", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 2, title: "10 seconds", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 3, title: "30 seconds", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 4, title: "1 minute", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 5, title: "5 minutes", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 6, title: "30 minutes", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 7, title: "1 hour", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 8, title: "6 hours", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 9, title: "12 hours", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 10, title: "1 day", isActive: false), +// ConversationDisappearingMessagesViewModel.Item(id: 11, title: "1 week", isActive: false) +// Item(id: 0, title: "Off", isActive: true), +// Item(id: 1, title: "5 seconds", isActive: false), +// Item(id: 2, title: "10 seconds", isActive: false), +// Item(id: 3, title: "30 seconds", isActive: false), +// Item(id: 4, title: "1 minute", isActive: false), +// Item(id: 5, title: "5 minutes", isActive: false), +// Item(id: 6, title: "30 minutes", isActive: false), +// Item(id: 7, title: "1 hour", isActive: false), +// Item(id: 8, title: "6 hours", isActive: false), +// Item(id: 9, title: "12 hours", isActive: false), +// Item(id: 10, title: "1 day", isActive: false), +// Item(id: 11, title: "1 week", isActive: false) +// ] +// +// viewModel = ConversationDisappearingMessagesViewModel(thread: thread, disappearingMessagesConfiguration: config) { [weak self] in +// self?.dataChangedCallbackTriggered = true +// } +// } +// +// override func tearDownWithError() throws { +// disposables = nil +// dataChangedCallbackTriggered = false +// thread = nil +// config = nil +// contact = nil +// defaultItems = nil +// viewModel = nil +// } +// +// // MARK: - ConversationDisappearingMessagesViewModel.Item +// +// func testItDefaultsToTheExistingValuesWhenUpdatedWithNullValues() throws { +// var item: ConversationDisappearingMessagesViewModel.Item = ConversationDisappearingMessagesViewModel.Item( +// id: 1, +// title: "Test", +// isActive: true +// ) +// +// expect(item.isActive).to(beTrue()) +// +// item = item.with(isActive: nil) +// expect(item.isActive).to(beTrue()) +// +// item = item.with(isActive: false) +// expect(item.isActive).to(beFalse()) +// } +// +// // MARK: - Basic Tests +// +// func testItHasTheCorrectTitle() throws { +// expect(self.viewModel.title).to(equal("DISAPPEARING_MESSAGES_SETTINGS_TITLE".localized())) +// } +// +// func testItHasTheCorrectDescriptionForAGroup() throws { +// thread = TSGroupThread(uniqueId: "TestId1") +// config = OWSDisappearingMessagesConfiguration(defaultWithThreadId: "TestId1") +// viewModel = ConversationDisappearingMessagesViewModel(thread: thread, disappearingMessagesConfiguration: config) { [weak self] in +// self?.dataChangedCallbackTriggered = true +// } +// +// expect(self.viewModel.description) +// .to(equal( +// String(format: NSLocalizedString("When enabled, messages between you and %@ will disappear after they have been seen.", comment: ""), arguments: ["the group"]) +// )) +// } +// +// func testItHasTheCorrectDescriptionForAKnownContact() throws { +// var hasWrittenToStorage: Bool = false +// +// // TODO: Mock storage +// Storage.write { [weak self] transaction in +// guard let strongSelf = self else { return } +// +// Storage.shared.setContact(strongSelf.contact, using: transaction) +// +// // Need to do these after setting the contact to ensure it's picked up correctly +// strongSelf.thread = TSContactThread(contactSessionID: "TestContactId") +// strongSelf.config = OWSDisappearingMessagesConfiguration(defaultWithThreadId: (strongSelf.thread.uniqueId ?? "TestContactId")) +// strongSelf.viewModel = ConversationDisappearingMessagesViewModel(thread: strongSelf.thread, disappearingMessagesConfiguration: strongSelf.config) { +// self?.dataChangedCallbackTriggered = true +// } +// hasWrittenToStorage = true +// } +// +// // Note: We need this to ensure the test doesn't run before the subsequent 'expect' doesn't +// // run before the viewModel gets recreated in the 'Storage.write' +// expect(hasWrittenToStorage) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// expect(self.viewModel.description) +// .toEventually( +// equal( +// String(format: NSLocalizedString("When enabled, messages between you and %@ will disappear after they have been seen.", comment: ""), arguments: ["anonymous"]) +// ), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItHasTheCorrectDescriptionForAKnownContactWithADisplayName() throws { +// var hasWrittenToStorage: Bool = false +// contact.nickname = "TestName" +// +// // TODO: Mock storage +// Storage.write { [weak self] transaction in +// guard let strongSelf = self else { return } +// +// Storage.shared.setContact(strongSelf.contact, using: transaction) +// +// // Need to do these after setting the contact to ensure it's picked up correctly +// strongSelf.thread = TSContactThread(contactSessionID: "TestContactId") +// strongSelf.config = OWSDisappearingMessagesConfiguration(defaultWithThreadId: (strongSelf.thread.uniqueId ?? "TestContactId")) +// strongSelf.viewModel = ConversationDisappearingMessagesViewModel(thread: strongSelf.thread, disappearingMessagesConfiguration: strongSelf.config) { +// self?.dataChangedCallbackTriggered = true +// } +// hasWrittenToStorage = true +// } +// +// // Note: We need this to ensure the test doesn't run before the subsequent 'expect' doesn't +// // run before the viewModel gets recreated in the 'Storage.write' +// expect(hasWrittenToStorage) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// expect(self.viewModel.description) +// .toEventually(equal( +// String(format: NSLocalizedString("When enabled, messages between you and %@ will disappear after they have been seen.", comment: ""), arguments: ["TestName"]) +// )) +// } +// +// func testItHasTheCorrectDescriptionForAnUnexpectedThreadType() throws { +// var hasWrittenToStorage: Bool = false +// contact.nickname = "TestName" +// +// // TODO: Mock storage +// Storage.write { [weak self] transaction in +// guard let strongSelf = self else { return } +// +// Storage.shared.setContact(strongSelf.contact, using: transaction) +// +// // Need to do these after setting the contact to ensure it's picked up correctly +// strongSelf.thread = TSThread(uniqueId: "TestId1") +// strongSelf.config = OWSDisappearingMessagesConfiguration(defaultWithThreadId: (strongSelf.thread.uniqueId ?? "TestId1")) +// strongSelf.viewModel = ConversationDisappearingMessagesViewModel(thread: strongSelf.thread, disappearingMessagesConfiguration: strongSelf.config) { +// self?.dataChangedCallbackTriggered = true +// } +// hasWrittenToStorage = true +// } +// +// // Note: We need this to ensure the test doesn't run before the subsequent 'expect' doesn't +// // run before the viewModel gets recreated in the 'Storage.write' +// expect(hasWrittenToStorage) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// expect(self.viewModel.description) +// .toEventually(equal( +// String(format: NSLocalizedString("When enabled, messages between you and %@ will disappear after they have been seen.", comment: ""), arguments: ["anonymous"]) +// )) +// } +// +// func testItHasTheCorrectNumberOfItems() throws { +// expect(self.viewModel.items.value.count).to(equal(12)) +// expect(self.viewModel.items.newest) +// .toEventually( +// haveCount(12), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItHasTheCorrectDefaultState() throws { +// expect(self.viewModel.items.value).to(equal(defaultItems)) +// expect(self.viewModel.items.newest) +// .toEventually( +// equal(defaultItems), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItStartsWithTheCorrectItemActiveIfNotDefault() throws { +// config = OWSDisappearingMessagesConfiguration(defaultWithThreadId: "TestId1") +// config.isEnabled = true +// config.durationSeconds = 30 +// viewModel = ConversationDisappearingMessagesViewModel(thread: thread, disappearingMessagesConfiguration: config) { [weak self] in +// self?.dataChangedCallbackTriggered = true +// } +// +// var nonDefaultItems: [ConversationDisappearingMessagesViewModel.Item] = defaultItems +// nonDefaultItems[0] = nonDefaultItems[0].with(isActive: false) +// nonDefaultItems[3] = nonDefaultItems[3].with(isActive: true) +// expect(self.viewModel.items.value).to(equal(nonDefaultItems)) +// } +// +// // MARK: - Interactions +// +// func testItProvidesTheThreadAndGivenDataWhenAnInteractionOccurs() throws { +// var interactionThread: TSThread? = nil +// +// self.viewModel.interaction.on(0) { thread in +// interactionThread = thread +// } +// +// self.viewModel.interaction.tap(0) +// +// expect(interactionThread).to(equal(self.thread)) +// } +// +// func testItRefreshesTheDataCorrectly() throws { +// expect(self.viewModel.items.value.count).to(beGreaterThan(3)) +// expect(self.viewModel.items.value[3].id).to(equal(3)) +// expect(self.viewModel.items.value[3].isActive).to(beFalse()) +// +// config.isEnabled = true +// config.durationSeconds = 30 +// +// viewModel.tryRefreshData(for: 3) +// +// expect(self.viewModel.items.value[3].id).to(equal(3)) +// expect(self.viewModel.items.value[3].isActive).to(beTrue()) +// } +// +// func testItDoesNotSetAnItemToActiveIfTheConfigIsNotEnabled() throws { +// expect(self.viewModel.items.value.count).to(beGreaterThan(3)) +// expect(self.viewModel.items.value[3].id).to(equal(3)) +// expect(self.viewModel.items.value[3].isActive).to(beFalse()) +// var nonDefaultItems: [Item] = defaultItems +// nonDefaultItems[0] = Item(id: nonDefaultItems[0].id, title: nonDefaultItems[0].title, isActive: false) +// nonDefaultItems[3] = Item(id: nonDefaultItems[3].id, title: nonDefaultItems[3].title, isActive: true) +// +// config.durationSeconds = 30 +// +// viewModel.tryRefreshData(for: 3) +// +// expect(self.viewModel.items.value[3].id).to(equal(3)) +// expect(self.viewModel.items.value[3].isActive).to(beFalse()) +// expect(self.viewModel.items.newest) +// .toEventually( +// equal(nonDefaultItems), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItUpdatesToADifferentValue() throws { +// expect(self.viewModel.items.value.count).to(beGreaterThan(3)) +// expect(self.viewModel.items.value[0].id).to(equal(0)) +// expect(self.viewModel.items.value[0].isActive).to(beTrue()) +// +// viewModel.interaction.tap(3) +// // MARK: - Interactions +// +// expect(self.viewModel.items.value[0].id) +// func testItSelectsTheItemCorrectly() throws { +// expect(self.viewModel.items.newest) +// .toEventually( +// equal(0), +// satisfyAllOf( +// haveCountGreaterThan(3), +// valueFor(\.id, at: 3, to: equal(3)), +// valueFor(\.isActive, at: 3, to: beFalse()) +// ), +// timeout: .milliseconds(100) +// ) +// expect(self.viewModel.items.value[0].isActive) +// +// viewModel.itemSelected.send(3) +// +// expect(self.viewModel.items.newest) +// .toEventually( +// beFalse(), +// satisfyAllOf( +// valueFor(\.id, at: 3, to: equal(3)), +// valueFor(\.isActive, at: 3, to: beTrue()) +// ), +// timeout: .milliseconds(100) +// ) +// expect(self.viewModel.items.value[3].id) +// } +// +// func testItUpdatesToADifferentValue() throws { +// expect(self.viewModel.items.newest) +// .toEventually( +// equal(3), +// satisfyAllOf( +// haveCountGreaterThan(3), +// valueFor(\.id, at: 0, to: equal(0)), +// valueFor(\.isActive, at: 0, to: beTrue()) +// ), +// timeout: .milliseconds(100) +// ) +// expect(self.viewModel.items.value[3].isActive) +// +// viewModel.itemSelected.send(3) +// +// expect(self.viewModel.items.newest) +// .toEventually( +// beTrue(), +// satisfyAllOf( +// valueFor(\.id, at: 0, to: equal(0)), +// valueFor(\.isActive, at: 0, to: beFalse()), +// valueFor(\.id, at: 3, to: equal(3)), +// valueFor(\.isActive, at: 3, to: beTrue()) +// ), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItUpdatesTheConfigWhenChangingValue() throws { +// // Note: Default for 'durationSectionds' is OWSDisappearingMessagesConfigurationDefaultExpirationDuration +// // currently set to 86400 +// expect(self.config.isEnabled).to(beFalse()) +// expect(self.config.durationSeconds).to(equal(86400)) +// +// viewModel.interaction.tap(3) +// +// viewModel.items.sink(receiveValue: { _ in }).store(in: &disposables) +// viewModel.itemSelected.send(3) +// +// expect(self.config.isEnabled) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// expect(self.config.durationSeconds) +// .toEventually( +// equal(30), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItDisablesTheConfigWhenSetToZero() throws { +// config.isEnabled = true +// +// viewModel.interaction.tap(0) +// +// viewModel.items.sink(receiveValue: { _ in }).store(in: &disposables) +// viewModel.itemSelected.send(0) +// +// expect(self.config.isEnabled) +// .toEventually( +// beFalse(), +// timeout: .milliseconds(100) +// ) +// expect(self.config.durationSeconds) +// .toEventually( +// equal(0), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItDoesNotSaveChangesIfTheConfigHasNotChangedFromItsDefaultState() { +// viewModel.trySaveChanges() +// +// +// // TODO: Mock out Storage.write +// expect(self.dataChangedCallbackTriggered) +// .toEventually( +// beFalse(), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItDoesSaveChangesIfTheConfigHasChanged() { +// config.isEnabled = true +// config.durationSeconds = 30 +// +// +// viewModel.trySaveChanges() +// +// +// // TODO: Mock out Storage.write +// expect(self.dataChangedCallbackTriggered) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// } +//} diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift new file mode 100644 index 000000000..c63c668b3 --- /dev/null +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -0,0 +1,767 @@ +//// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +//import Combine +//import Quick +//import Nimble +// +//@testable import Session +// +//class ThreadSettingsViewModelSpec: QuickSpec { +// typealias Item = ConversationSettingsViewModel.Item +// typealias ActionableItem = ConversationSettingsViewModel.ActionableItem +// typealias NavItem = ConversationSettingsViewModel.NavItem +// +// var disposables: Set! +// var didTriggerSearchCallbackTriggered: Bool = false +// var publicKey: String! +// var thread: TSThread! +// var uiDatabaseConnection: YapDatabaseConnection! +// var defaultContactThreadItems: [[Item]]! +// var viewModel: ConversationSettingsViewModel! +// +// +// // MARK: - Configuration +// +// override func setUpWithError() throws { +// didTriggerSearchCallbackTriggered = false +// +// // TODO: Need to mock TSThread, YapDatabaseConnection and the publicKey retrieval logic +// disposables = Set() +// didTriggerSearchCallbackTriggered = false +// publicKey = SNGeneralUtilities.getUserPublicKey() +// thread = TSContactThread(contactSessionID: "TestContactId") +// uiDatabaseConnection = OWSPrimaryStorage.shared().uiDatabaseConnection +// defaultContactThreadItems = [ +// [ +// Item( +// id: .header, +// style: .header, +// title: "Anonymous", +// subtitle: "TestContactId" +// ) +// ], +// [ +// Item( +// id: .search, +// style: .search, +// icon: UIImage(named: "conversation_settings_search")?.withRenderingMode(.alwaysTemplate), +// title: "CONVERSATION_SETTINGS_SEARCH".localized(), +// accessibilityIdentifier: "ConversationSettingsViewModel.search" +// ) +// ], +// [ +// Item( +// id: .allMedia, +// icon: UIImage(named: "actionsheet_camera_roll_black")?.withRenderingMode(.alwaysTemplate), +// title: MediaStrings.allMedia, +// accessibilityIdentifier: "ConversationSettingsViewModel.all_media" +// ), +// Item( +// id: .pinConversation, +// icon: UIImage(named: "settings_pin")?.withRenderingMode(.alwaysTemplate), +// title: "CONVERSATION_SETTINGS_PIN".localized(), +// accessibilityIdentifier: "ConversationSettingsViewModel.pin_conversation" +// ), +// Item( +// id: .disappearingMessages, +// icon: UIImage(named: "timer_55")?.withRenderingMode(.alwaysTemplate), +// title: "DISAPPEARING_MESSAGES".localized(), +// subtitle: "DISAPPEARING_MESSAGES_OFF".localized(), +// accessibilityIdentifier: "ConversationSettingsViewModel.disappearing_messages" +// ), +// Item( +// id: .notifications, +// icon: UIImage(named: "mute_unfilled")?.withRenderingMode(.alwaysTemplate), +// title: "CONVERSATION_SETTINGS_MUTE_ACTION_NEW".localized(), +// accessibilityIdentifier: "ConversationSettingsViewModel.mute" +// ) +// ], +// [ +// Item( +// id: .deleteMessages, +// icon: UIImage(named: "trash")?.withRenderingMode(.alwaysTemplate), +// title: "DELETE_MESSAGES".localized(), +// isNegativeAction: true, +// accessibilityIdentifier: "ConversationSettingsViewModel.delete_messages" +// ), +// Item( +// id: .blockUser, +// icon: UIImage(named: "table_ic_block")?.withRenderingMode(.alwaysTemplate), +// title: "CONVERSATION_SETTINGS_BLOCK_USER".localized(), +// isNegativeAction: true, +// accessibilityIdentifier: "ConversationSettingsViewModel.block" +// ) +// ] +// ] +// +// viewModel = ConversationSettingsViewModel(thread: thread, uiDatabaseConnection: uiDatabaseConnection, didTriggerSearch: { [weak self] in +// self?.didTriggerSearchCallbackTriggered = true +// }) +// } +// +// +// override func tearDownWithError() throws { +// disposables = nil +// didTriggerSearchCallbackTriggered = false +// publicKey = nil +// thread = nil +// uiDatabaseConnection = nil +// defaultContactThreadItems = nil +// viewModel = nil +// } +// +// // MARK: - Basic Tests +// // MARK: - Item +// +// func testTheItemGetsCreatedCorrectly() { +// let image: UIImage = UIImage() +// let item: Item = Item( +// id: .allMedia, +// style: .header, +// icon: image, +// title: "Test", +// subtitle: "TestSub", +// isEnabled: false, +// isEditing: true, +// isNegativeAction: true, +// accessibilityIdentifier: "TestAccessibility" +// ) +// +// expect(item.id).to(equal(.allMedia)) +// expect(item.style).to(equal(.header)) +// expect(item.icon).to(equal(image)) +// expect(item.title).to(equal("Test")) +// expect(item.subtitle).to(equal("TestSub")) +// expect(item.isEnabled).to(beFalse()) +// expect(item.isEditing).to(beTrue()) +// expect(item.isNegativeAction).to(beTrue()) +// expect(item.accessibilityIdentifier).to(equal("TestAccessibility")) +// } +// +// func testTheItemHasTheCorrectDefaultValues() { +// let item: Item = Item(id: .allMedia) +// +// expect(item.id).to(equal(.allMedia)) +// expect(item.style).to(equal(.standard)) +// expect(item.icon).to(beNil()) +// expect(item.title).to(equal("")) +// expect(item.subtitle).to(beNil()) +// expect(item.isEnabled).to(beTrue()) +// expect(item.isEditing).to(beFalse()) +// expect(item.isNegativeAction).to(beFalse()) +// expect(item.accessibilityIdentifier).to(beNil()) +// } +// +// // MARK: - ActionableItem +// +// func testTheActionableItemGetsCreatedCorrectly() { +// let item: Item = Item(id: .allMedia) +// let subject: PassthroughSubject = PassthroughSubject() +// let actionableItem: ActionableItem = ActionableItem( +// data: item, +// action: subject +// ) +// +// expect(actionableItem.data).to(equal(item)) +// expect(actionableItem.action).to(beIdenticalTo(subject)) +// } +// +// // MARK: - Basic Tests +// +// func testItHasTheCorrectTitleForAnIndividualThread() { +// expect(self.viewModel.title).to(equal("vc_settings_title".localized())) +// } +// +// +// func testItHasTheCorrectTitleForAGroupThread() { +// thread = TSGroupThread(uniqueId: "TestGroupId1") +// viewModel = ConversationSettingsViewModel(thread: thread, uiDatabaseConnection: uiDatabaseConnection, didTriggerSearch: { [weak self] in +// self?.didTriggerSearchCallbackTriggered = true +// }) +// +// +// expect(self.viewModel.title).to(equal("vc_group_settings_title".localized())) +// } +// +// +// // MARK: - All Conversation Type Shared Tests +// +// +// func testItTriggersTheSearchCallbackWhenInteractingWithSearch() { +// viewModel.interaction.tap(.search) +// +// expect(self.didTriggerSearchCallbackTriggered).to(beTrue()) +// viewModel.viewSearch.sink(receiveValue: { _ in }).store(in: &disposables) +// viewModel.searchTapped.send() +// +// expect(self.didTriggerSearchCallbackTriggered) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItPinsAConversation() { +// viewModel.interaction.tap(.togglePinConversation) +// +// viewModel.items.sink(receiveValue: { _ in }).store(in: &disposables) +// viewModel.pinConversationTapped.send() +// +// expect(self.thread.isPinned) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItUnPinsAConversation() { +// viewModel.interaction.tap(.togglePinConversation) +// thread.isPinned = true +// +// viewModel.items.sink(receiveValue: { _ in }).store(in: &disposables) +// viewModel.pinConversationTapped.send() +// +// expect(self.thread.isPinned) +// .toEventually( +// beTrue(), +// beFalse(), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItUpdatesTheItemTitleToReflectThePinnedState() { +// thread.isPinned = true +// +// viewModel.interaction.tap(.togglePinConversation) +// let itemsData = viewModel.items +// .map { sections in sections.map { section in section.map { $0.data } } } +// +// expect(self.thread.isPinned) +// expect(itemsData.newest) +// .toEventually( +// beFalse(), +// satisfyAllOf( +// haveCountGreaterThan(2), +// valueAt(2, haveCountGreaterThan(1)) +// ), +// timeout: .milliseconds(100) +// ) +// expect(itemsData.map { $0[2][1].title }.newest) +// .toEventually( +// equal("CONVERSATION_SETTINGS_UNPIN".localized()), +// timeout: .milliseconds(10000) +// ) +// } +// +// func testDeletingMessageShowsAndThensHidesTheLoadingState() { +// let replayLoadingState = viewModel.loadingStateVisible.shareReplay(2) +// replayLoadingState.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.deleteMessages() +// +// expect(replayLoadingState.all) +// .toEventually( +// equal([ +// true, +// false +// ]), +// timeout: .milliseconds(100) +// ) +// } +// +// // MARK: - Individual & Note to Self Conversation Shared Tests +// +// +// func testItHasTheCorrectDefaultNavButtonsForAContactConversation() { +// expect(self.viewModel.leftNavItems.value).to(equal([])) +// expect(self.viewModel.rightNavItems.value) +// .to(equal([ +// ConversationSettingsViewModel.Item( +// id: .navEdit, +// style: .navigation, +// action: .startEditingDisplayName, +// icon: nil, +// title: "", +// barButtonItem: .edit, +// subtitle: nil, +// isEnabled: true, +// isNegativeAction: false, +// accessibilityIdentifier: "Edit button" +// ) +// ])) +// expect(self.viewModel.leftNavItems.newest) +// .toEventually( +// haveCount(0), +// timeout: .milliseconds(100) +// ) +// expect(self.viewModel.rightNavItems.map { items in items.map { $0.data } }.newest) +// .toEventually( +// equal([ +// NavItem( +// systemItem: .edit, +// accessibilityIdentifier: "Edit button" +// ) +// ]), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItUpdatesTheNavButtonsWhenEnteringEditMode() { +// viewModel.interaction.tap(.startEditingDisplayName) +// +// expect(self.viewModel.leftNavItems.value) +// .to(equal([ +// ConversationSettingsViewModel.Item( +// id: .navCancel, +// style: .navigation, +// action: .cancelEditingDisplayName, +// icon: nil, +// title: "", +// barButtonItem: .cancel, +// subtitle: nil, +// isEnabled: true, +// isNegativeAction: false, +// accessibilityIdentifier: "Cancel button" +// ) +// ])) +// expect(self.viewModel.rightNavItems.value) +// .to(equal([ +// ConversationSettingsViewModel.Item( +// id: .navDone, +// style: .navigation, +// action: .saveUpdatedDisplayName, +// icon: nil, +// title: "", +// barButtonItem: .done, +// subtitle: nil, +// isEnabled: true, +// isNegativeAction: false, +// accessibilityIdentifier: "Done button" +// ) +// ])) +// let replayLeftNavItems = viewModel.leftNavItems.map { items in items.map { $0.data } }.shareReplay(1) +// let replayRightNavItems = viewModel.rightNavItems.map { items in items.map { $0.data } }.shareReplay(1) +// replayLeftNavItems.sink(receiveValue: { _ in }).store(in: &disposables) +// replayRightNavItems.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.editDisplayNameTapped.send() +// +// expect(replayLeftNavItems.newest) +// .toEventually( +// equal([ +// NavItem( +// systemItem: .cancel, +// accessibilityIdentifier: "Cancel button" +// ) +// ]), +// timeout: .milliseconds(100) +// ) +// expect(replayRightNavItems.newest) +// .toEventually( +// equal([ +// NavItem( +// systemItem: .done, +// accessibilityIdentifier: "Done button" +// ) +// ]), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItGoesBackToTheDefaultNavButtonsWhenYouCancelEditingTheDisplayName() { +// viewModel.interaction.tap(.startEditingDisplayName) +// +// expect(self.viewModel.leftNavItems.value.first?.id).to(equal(.navCancel)) +// +// viewModel.interaction.tap(.cancelEditingDisplayName) +// +// expect(self.viewModel.leftNavItems.value).to(equal([])) +// expect(self.viewModel.rightNavItems.value) +// .to(equal([ +// ConversationSettingsViewModel.Item( +// id: .navEdit, +// style: .navigation, +// action: .startEditingDisplayName, +// icon: nil, +// title: "", +// barButtonItem: .edit, +// subtitle: nil, +// isEnabled: true, +// isNegativeAction: false, +// accessibilityIdentifier: "Edit button" +// ) +// ])) +// let replayLeftNavItems = viewModel.leftNavItems.map { items in items.map { $0.data } }.shareReplay(1) +// let replayRightNavItems = viewModel.rightNavItems.map { items in items.map { $0.data } }.shareReplay(1) +// replayLeftNavItems.sink(receiveValue: { _ in }).store(in: &disposables) +// replayRightNavItems.sink(receiveValue: { _ in }).store(in: &disposables) +// +// // Change to editing state +// viewModel.editDisplayNameTapped.send() +// +// expect(replayLeftNavItems.newest) +// .toEventually( +// valueFor(\.systemItem, at: 0, to: equal(.cancel)), +// timeout: .milliseconds(100) +// ) +// +// // Change back +// viewModel.cancelEditDisplayNameTapped.send() +// +// expect(replayLeftNavItems.newest) +// .toEventually( +// haveCount(0), +// timeout: .milliseconds(100) +// ) +// expect(replayRightNavItems.newest) +// .toEventually( +// equal([ +// NavItem( +// systemItem: .edit, +// accessibilityIdentifier: "Edit button" +// ) +// ]), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItGoesBackToTheDefaultNavButtonsWhenYouSaveTheUpdatedDisplayName() { +// viewModel.interaction.tap(.startEditingDisplayName) +// +// expect(self.viewModel.leftNavItems.value.first?.id).to(equal(.navCancel)) +// +// viewModel.interaction.tap(.saveUpdatedDisplayName) +// +// expect(self.viewModel.leftNavItems.value).to(equal([])) +// expect(self.viewModel.rightNavItems.value) +// .to(equal([ +// ConversationSettingsViewModel.Item( +// id: .navEdit, +// style: .navigation, +// action: .startEditingDisplayName, +// icon: nil, +// title: "", +// barButtonItem: .edit, +// subtitle: nil, +// isEnabled: true, +// isNegativeAction: false, +// accessibilityIdentifier: "Edit button" +// ) +// ])) +// let replayLeftNavItems = viewModel.leftNavItems.map { items in items.map { $0.data } }.shareReplay(1) +// let replayRightNavItems = viewModel.rightNavItems.map { items in items.map { $0.data } }.shareReplay(1) +// replayLeftNavItems.sink(receiveValue: { _ in }).store(in: &disposables) +// replayRightNavItems.sink(receiveValue: { _ in }).store(in: &disposables) +// +// // Change to editing state +// viewModel.editDisplayNameTapped.send() +// +// expect(replayLeftNavItems.newest) +// .toEventually( +// valueFor(\.systemItem, at: 0, to: equal(.cancel)), +// timeout: .milliseconds(100) +// ) +// +// // Change back +// viewModel.saveDisplayNameTapped.send() +// +// expect(replayLeftNavItems.newest) +// .toEventually( +// haveCount(0), +// timeout: .milliseconds(100) +// ) +// expect(replayRightNavItems.newest) +// .toEventually( +// equal([ +// NavItem( +// systemItem: .edit, +// accessibilityIdentifier: "Edit button" +// ) +// ]), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItHasTheCorrectDefaultState() throws { +// let itemsData = viewModel.items +// .map { sections in sections.map { section in section.map { $0.data } } } +// +// expect(itemsData.newest) +// .toEventually( +// equal(defaultContactThreadItems), +// timeout: .milliseconds(1000) +// ) +// } +// +// func testItUpdatesTheContactNicknameWhenSavingTheUpdatedDisplayName() { +// viewModel.interaction.tap(.startEditingDisplayName) +// viewModel.interaction.change(.changeDisplayName, data: "Test123") +// viewModel.interaction.tap(.saveUpdatedDisplayName) +// viewModel.leftNavItems.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.displayName = "Test123" +// viewModel.saveDisplayNameTapped.send() +// +// expect(Storage.shared.getContact(with: "TestContactId")?.nickname) +// .toEventually( +// equal("Test123"), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItMutesAConversation() { +// viewModel.interaction.tap(.toggleMuteNotifications) +// +// +// func testItMutesAContactConversation() { +// viewModel.items.sink(receiveValue: { _ in }).store(in: &disposables) +// viewModel.notificationsTapped.send() +// +// expect(self.thread.isMuted) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// } +// +// +// func testItUnMutesAConversation() { +// viewModel.interaction.tap(.toggleMuteNotifications) +// var hasWrittenToStorage: Bool = false +// +// Storage.write { transaction in +// self.thread.updateWithMuted( +// until: Date.distantFuture, +// transaction: transaction +// ) +// hasWrittenToStorage = true +// } +// +// // Note: Wait for the setup to complete +// expect(hasWrittenToStorage) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// expect(self.thread.isMuted) +// .toEventually( +// beTrue(), +// timeout: .milliseconds(100) +// ) +// +// viewModel.interaction.tap(.toggleMuteNotifications) +// +// viewModel.items.sink(receiveValue: { _ in }).store(in: &disposables) +// viewModel.notificationsTapped.send() +// +// expect(self.thread.isMuted) +// .toEventually( +// beFalse(), +// timeout: .milliseconds(100) +// ) +// } +// +// +// // MARK: - Group Conversation Tests +// +// +// func testItHasNoCustomLeftNavButtons() { +// thread = TSGroupThread(uniqueId: "TestGroupId1") +// viewModel = ConversationSettingsViewModel(thread: thread, uiDatabaseConnection: uiDatabaseConnection, didTriggerSearch: { [weak self] in +// self?.didTriggerSearchCallbackTriggered = true +// }) +// +// expect(self.viewModel.leftNavItems.newest) +// .toEventually( +// haveCount(0), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItHasNoCustomRightNavButtons() { +// thread = TSGroupThread(uniqueId: "TestGroupId1") +// viewModel = ConversationSettingsViewModel(thread: thread, uiDatabaseConnection: uiDatabaseConnection, didTriggerSearch: { [weak self] in +// self?.didTriggerSearchCallbackTriggered = true +// }) +// +// expect(self.viewModel.rightNavItems.newest) +// .toEventually( +// haveCount(0), +// timeout: .milliseconds(100) +// ) +// } +// +// func testLeavingGroupShowsAndThensHidesTheLoadingState() { +// thread = TSGroupThread(uniqueId: "TestGroupId1") +// (thread as? TSGroupThread)?.groupModel = TSGroupModel( +// title: nil, +// memberIds: [], +// image: nil, +// groupId: "".data(using: .utf8)!, +// groupType: .closedGroup, +// adminIds: [] +// ) +// viewModel = ConversationSettingsViewModel(thread: thread, uiDatabaseConnection: uiDatabaseConnection, didTriggerSearch: { [weak self] in +// self?.didTriggerSearchCallbackTriggered = true +// }) +// +// let replayLoadingState = viewModel.loadingStateVisible.shareReplay(2) +// replayLoadingState.sink(receiveValue: { _ in }).store(in: &disposables) +// +// expect(self.viewModel.leftNavItems.value).to(equal([])) +// viewModel.leaveGroup() +// +// expect(replayLoadingState.all) +// .toEventually( +// equal([ +// true//, +// //false // TODO: Need to mock MessageSender for this to work +// ]), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItHasNoCustomRightNavButtons() { +// // MARK: - Transitions +// +// func testItViewsTheSearch() { +// let replayViewSearch = viewModel.viewSearch.shareReplay(1) +// replayViewSearch.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.searchTapped.send() +// +// expect(replayViewSearch.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItViewsAddToGroup() { +// let replayViewAddToGroup = viewModel.viewAddToGroup.shareReplay(1) +// replayViewAddToGroup.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.addToGroupTapped.send() +// +// expect(replayViewAddToGroup.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItViewsEditGroup() { +// let replayViewEditGroup = viewModel.viewEditGroup.shareReplay(1) +// replayViewEditGroup.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.editGroupTapped.send() +// +// expect(replayViewEditGroup.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItViewsAllMedia() { +// let replayViewAllMedia = viewModel.viewAllMedia.shareReplay(1) +// replayViewAllMedia.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.viewAllMediaTapped.send() +// +// expect(replayViewAllMedia.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItViewsDisappearingMessages() { +// let replayViewDisappearingMessages = viewModel.viewDisappearingMessages.shareReplay(1) +// replayViewDisappearingMessages.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.disappearingMessagesTapped.send() +// +// expect(replayViewDisappearingMessages.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItViewsNotificationSettings() { +// thread = TSGroupThread(uniqueId: "TestGroupId1") +// viewModel = ConversationSettingsViewModel(thread: thread, uiDatabaseConnection: uiDatabaseConnection, didTriggerSearch: { [weak self] in +// self?.didTriggerSearchCallbackTriggered = true +// }) +// +// let replayViewNotificationSettings = viewModel.viewNotificationSettings.shareReplay(1) +// replayViewNotificationSettings.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.notificationsTapped.send() +// +// expect(replayViewNotificationSettings.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItShowsTheDeleteMessagesAlert() { +// let replayViewDeleteMessagesAlert = viewModel.viewDeleteMessagesAlert.shareReplay(1) +// replayViewDeleteMessagesAlert.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.deleteMessagesTapped.send() +// +// expect(replayViewDeleteMessagesAlert.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItShowsTheLeaveGroupAlert() { +// thread = TSGroupThread(uniqueId: "TestGroupId1") +// viewModel = ConversationSettingsViewModel(thread: thread, uiDatabaseConnection: uiDatabaseConnection, didTriggerSearch: { [weak self] in +// self?.didTriggerSearchCallbackTriggered = true +// }) +// +// let replayViewLeaveGroupAlert = viewModel.viewLeaveGroupAlert.shareReplay(1) +// replayViewLeaveGroupAlert.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.leaveGroupTapped.send() +// +// expect(replayViewLeaveGroupAlert.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// func testItShowsTheBlockUserAlert() { +// let replayViewBlockUserAlert = viewModel.viewBlockUserAlert.shareReplay(1) +// replayViewBlockUserAlert.sink(receiveValue: { _ in }).store(in: &disposables) +// +// viewModel.blockTapped.send() +// +// expect(self.viewModel.rightNavItems.value).to(equal([])) +// expect(replayViewBlockUserAlert.all) +// .toEventually( +// haveCount(1), +// timeout: .milliseconds(100) +// ) +// } +// +// // TODO: Mock 'OWSProfileManager' to test 'viewProfilePicture' +// // TODO: Various item states depending on thread type +// // TODO: Group title options (need mocking?) +// // TODO: Notification item title options (need mocking?) +// // TODO: Delete All Messages (need mocking) +// // TODO: Add to Group (need mocking) +// // TODO: Leave Group (need mocking) +//} diff --git a/SessionTests/SessionTests.swift b/SessionTests/SessionTests.swift new file mode 100644 index 000000000..3c22a0ce3 --- /dev/null +++ b/SessionTests/SessionTests.swift @@ -0,0 +1,30 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import XCTest + +class SessionTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift new file mode 100644 index 000000000..8a15a6319 --- /dev/null +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -0,0 +1,157 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Combine +import GRDB +import Quick +import Nimble + +@testable import Session + +class NotificationContentViewModelSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + var mockStorage: Storage! + var dataChangeCancellable: AnyCancellable? + var viewModel: NotificationContentViewModel! + + describe("a NotificationContentViewModel") { + // MARK: - Configuration + + beforeEach { + mockStorage = Storage( + customWriter: DatabaseQueue(), + customMigrations: [ + SNUtilitiesKit.migrations(), + SNSnodeKit.migrations(), + SNMessagingKit.migrations(), + SUIKit.migrations() + ] + ) + viewModel = NotificationContentViewModel(storage: mockStorage) + dataChangeCancellable = viewModel.observableSettingsData + .receiveOnMain(immediately: true) + .sink( + receiveCompletion: { _ in }, + receiveValue: { viewModel.updateSettings($0) } + ) + } + + afterEach { + dataChangeCancellable?.cancel() + + mockStorage = nil + dataChangeCancellable = nil + viewModel = nil + } + + // MARK: - Basic Tests + + it("has the correct title") { + expect(viewModel.title).to(equal("NOTIFICATIONS_STYLE_CONTENT_TITLE".localized())) + } + + it("has the correct number of items") { + expect(viewModel.settingsData.count) + .toEventually( + equal(1), + timeout: .milliseconds(10) + ) + expect(viewModel.settingsData.first?.elements.count) + .toEventually( + equal(3), + timeout: .milliseconds(10) + ) + } + + it("has the correct default state") { + expect(viewModel.settingsData.first?.elements ) + .toEventually( + equal([ + SettingInfo( + id: Preferences.NotificationPreviewType.nameAndPreview, + title: "NOTIFICATIONS_SENDER_AND_MESSAGE".localized(), + action: .listSelection( + isSelected: { true }, + storedSelection: true, + shouldAutoSave: true, + selectValue: {} + ) + ), + SettingInfo( + id: Preferences.NotificationPreviewType.nameNoPreview, + title: "NOTIFICATIONS_SENDER_ONLY".localized(), + action: .listSelection( + isSelected: { false }, + storedSelection: false, + shouldAutoSave: true, + selectValue: {} + ) + ), + SettingInfo( + id: Preferences.NotificationPreviewType.noNameNoPreview, + title: "NOTIFICATIONS_NONE".localized(), + action: .listSelection( + isSelected: { false }, + storedSelection: false, + shouldAutoSave: true, + selectValue: {} + ) + ) + ]), + timeout: .milliseconds(10) + ) + } + + it("starts with the correct item active if not default") { + mockStorage.write { db in + db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType.nameNoPreview + } + viewModel = NotificationContentViewModel(storage: mockStorage) + dataChangeCancellable = viewModel.observableSettingsData + .receiveOnMain(immediately: true) + .sink( + receiveCompletion: { _ in }, + receiveValue: { viewModel.updateSettings($0) } + ) + + expect(viewModel.settingsData.first?.elements ) + .toEventually( + equal([ + SettingInfo( + id: Preferences.NotificationPreviewType.nameAndPreview, + title: "NOTIFICATIONS_SENDER_AND_MESSAGE".localized(), + action: .listSelection( + isSelected: { false }, + storedSelection: false, + shouldAutoSave: true, + selectValue: {} + ) + ), + SettingInfo( + id: Preferences.NotificationPreviewType.nameNoPreview, + title: "NOTIFICATIONS_SENDER_ONLY".localized(), + action: .listSelection( + isSelected: { true }, + storedSelection: true, + shouldAutoSave: true, + selectValue: {} + ) + ), + SettingInfo( + id: Preferences.NotificationPreviewType.noNameNoPreview, + title: "NOTIFICATIONS_NONE".localized(), + action: .listSelection( + isSelected: { false }, + storedSelection: false, + shouldAutoSave: true, + selectValue: {} + ) + ) + ]), + timeout: .milliseconds(10) + ) + } + } + } +} diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift index eab508164..30dc56d71 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift @@ -90,6 +90,9 @@ internal enum Theme_ClassicDark: ThemeColors { .conversationButton_swipeSecondary: .classicDark2, .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color, + // InputButton + .inputButton_background: .classicDark1, + // Call .callAccept_background: Theme.PrimaryColor.green.color, .callDecline_background: .dangerDark diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift index 7d7903838..e8cb6286b 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift @@ -90,6 +90,9 @@ internal enum Theme_ClassicLight: ThemeColors { .conversationButton_swipeSecondary: .classicLight1, .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color, + // InputButton + .inputButton_background: .classicDark6, + // Call .callAccept_background: Theme.PrimaryColor.green.color, .callDecline_background: .dangerLight diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift index 8769714ca..11c17219d 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift @@ -80,9 +80,9 @@ internal enum Theme_OceanDark: ThemeColors { // ConversationButton .conversationButton_background: .oceanDark3, - .conversationButton_highlight: .oceanDark3, + .conversationButton_highlight: .oceanDark4, .conversationButton_unreadBackground: .oceanDark2, - .conversationButton_unreadHighlight: .oceanDark3, + .conversationButton_unreadHighlight: .oceanDark4, .conversationButton_unreadStripBackground: .primary, .conversationButton_unreadBubbleBackground: .primary, .conversationButton_unreadBubbleText: .oceanDark0, @@ -90,6 +90,9 @@ internal enum Theme_OceanDark: ThemeColors { .conversationButton_swipeSecondary: .oceanDark2, .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color, + // InputButton + .inputButton_background: .oceanDark4, + // Call .callAccept_background: Theme.PrimaryColor.green.color, .callDecline_background: .dangerDark diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift index 8c84525cd..5ff01aecb 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift @@ -90,6 +90,9 @@ internal enum Theme_OceanLight: ThemeColors { .conversationButton_swipeSecondary: .oceanLight1, .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color, + // InputButton + .inputButton_background: .oceanLight6, + // Call .callAccept_background: Theme.PrimaryColor.green.color, .callDecline_background: .dangerLight diff --git a/SessionUIKit/Style Guide/Themes/Theme.swift b/SessionUIKit/Style Guide/Themes/Theme.swift index 0ff16f1d5..91c5cb0f9 100644 --- a/SessionUIKit/Style Guide/Themes/Theme.swift +++ b/SessionUIKit/Style Guide/Themes/Theme.swift @@ -141,6 +141,9 @@ public enum ThemeValue { case conversationButton_swipeSecondary case conversationButton_swipeTertiary + // InputButton + case inputButton_background + // Call case callAccept_background case callDecline_background