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
This commit is contained in:
Morgan Pretty 2022-09-13 17:57:13 +10:00
parent 93c2f0f503
commit fe14bb1b31
25 changed files with 2032 additions and 181 deletions

View File

@ -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

View File

@ -242,6 +242,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 6d640ed0e238b2eab2cf98ed1e125394b41041eb
PODFILE CHECKSUM: a646db9086664b8c9e5a0b3d17664a1275dcba9d
COCOAPODS: 1.11.3

View File

@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@ -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 = "<group>"; };
B657DDC91911A40500F45B0C /* Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Signal.entitlements; sourceTree = "<group>"; };
@ -1798,6 +1813,11 @@
FD7115FF28C8253500B47552 /* UIView+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Combine.swift"; sourceTree = "<group>"; };
FD71160128C8255900B47552 /* UIControl+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Combine.swift"; sourceTree = "<group>"; };
FD71160328C95B5600B47552 /* PhotoCollectionPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerViewModel.swift; sourceTree = "<group>"; };
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 = "<group>"; };
FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesViewModelSpec.swift; sourceTree = "<group>"; };
FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModelSpec.swift; sourceTree = "<group>"; };
FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModelSpec.swift; sourceTree = "<group>"; };
FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = "<group>"; };
FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManagerProtocol.swift; sourceTree = "<group>"; };
FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentCallProtocol.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 = "<group>";
@ -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 = "<group>";
@ -3798,8 +3831,8 @@
FD3C906B27E43C2400CD579F /* Sending & Receiving */ = {
isa = PBXGroup;
children = (
FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */,
FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */,
FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */,
);
path = "Sending & Receiving";
sourceTree = "<group>";
@ -3842,6 +3875,41 @@
path = "Disposable Views";
sourceTree = "<group>";
};
FD71160A28D00BAE00B47552 /* SessionTests */ = {
isa = PBXGroup;
children = (
FD71161228D00D5300B47552 /* Conversations */,
FD71161828D00E0100B47552 /* Settings */,
FD71160B28D00BAE00B47552 /* SessionTests.swift */,
);
path = SessionTests;
sourceTree = "<group>";
};
FD71161228D00D5300B47552 /* Conversations */ = {
isa = PBXGroup;
children = (
FD71161328D00D5D00B47552 /* Settings */,
);
path = Conversations;
sourceTree = "<group>";
};
FD71161328D00D5D00B47552 /* Settings */ = {
isa = PBXGroup;
children = (
FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */,
FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */,
);
path = Settings;
sourceTree = "<group>";
};
FD71161828D00E0100B47552 /* Settings */ = {
isa = PBXGroup;
children = (
FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */,
);
path = Settings;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = (

View File

@ -20,6 +20,20 @@
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FD71160828D00BAE00B47552"
BuildableName = "SessionTests.xctest"
BlueprintName = "SessionTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
@ -128,6 +142,18 @@
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FD71160828D00BAE00B47552"
BuildableName = "SessionTests.xctest"
BlueprintName = "SessionTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -10,10 +10,19 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
private var previousY: CGFloat = 0
let call: SessionCall
// MARK: UI Components
// MARK: - UI Components
private lazy var backgroundView: UIView = {
let result: UIView = UIView()
result.themeBackgroundColor = .black
result.alpha = 0.8
return result
}()
private lazy var profilePictureView: ProfilePictureView = {
let result = ProfilePictureView()
let size = CGFloat(60)
let size: CGFloat = 60
result.size = size
result.set(.width, to: size)
result.set(.height, to: size)
@ -22,53 +31,72 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
private lazy var displayNameLabel: UILabel = {
let result = UILabel()
result.textColor = UIColor.white
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .white
result.lineBreakMode = .byTruncatingTail
return result
}()
private lazy var answerButton: UIButton = {
let result = UIButton(type: .custom)
let image = UIImage(named: "AnswerCall")!.withTint(.white)?.resizedImage(to: CGSize(width: 24.8, height: 24.8))
result.setImage(image, for: UIControl.State.normal)
result.setImage(
UIImage(named: "AnswerCall")?
.resizedImage(to: CGSize(width: 24.8, height: 24.8))?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .white
result.themeBackgroundColor = .callAccept_background
result.layer.cornerRadius = 24
result.addTarget(self, action: #selector(answerCall), for: .touchUpInside)
result.set(.width, to: 48)
result.set(.height, to: 48)
result.backgroundColor = Colors.accent
result.layer.cornerRadius = 24
result.addTarget(self, action: #selector(answerCall), for: UIControl.Event.touchUpInside)
return result
}()
private lazy var hangUpButton: UIButton = {
let result = UIButton(type: .custom)
let image = UIImage(named: "EndCall")!.withTint(.white)?.resizedImage(to: CGSize(width: 29.6, height: 11.2))
result.setImage(image, for: UIControl.State.normal)
result.setImage(
UIImage(named: "EndCall")?
.resizedImage(to: CGSize(width: 29.6, height: 11.2))?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .white
result.themeBackgroundColor = .callDecline_background
result.layer.cornerRadius = 24
result.addTarget(self, action: #selector(endCall), for: .touchUpInside)
result.set(.width, to: 48)
result.set(.height, to: 48)
result.backgroundColor = Colors.destructive
result.layer.cornerRadius = 24
result.addTarget(self, action: #selector(endCall), for: UIControl.Event.touchUpInside)
return result
}()
private lazy var panGestureRecognizer: UIPanGestureRecognizer = {
let result = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
result.delegate = self
return result
}()
// MARK: Initialization
// MARK: - Initialization
public static var current: IncomingCallBanner?
init(for call: SessionCall) {
self.call = call
super.init(frame: CGRect.zero)
setUpViewHierarchy()
setUpGestureRecognizers()
if let incomingCallBanner = IncomingCallBanner.current {
incomingCallBanner.dismiss()
}
IncomingCallBanner.current = self
}
@ -81,22 +109,26 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
}
private func setUpViewHierarchy() {
self.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(0.8)
self.clipsToBounds = true
self.layer.cornerRadius = Values.largeSpacing
self.layer.masksToBounds = true
self.set(.height, to: 100)
addSubview(backgroundView)
backgroundView.pin(to: self)
profilePictureView.update(
publicKey: call.sessionId,
profile: Profile.fetchOrCreate(id: call.sessionId),
threadVariant: .contact
)
displayNameLabel.text = call.contactName
let stackView = UIStackView(arrangedSubviews: [profilePictureView, displayNameLabel, hangUpButton, answerButton])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = Values.largeSpacing
self.addSubview(stackView)
stackView.center(.vertical, in: self)
stackView.autoPinWidthToSuperview(withMargin: Values.mediumSpacing)
}
@ -108,14 +140,16 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
addGestureRecognizer(panGestureRecognizer)
}
// MARK: Interaction
// MARK: - Interaction
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> 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

View File

@ -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

View File

@ -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)

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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?) { }
}

View File

@ -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)

View File

@ -8,6 +8,14 @@ import SessionMessagingKit
import SessionUtilitiesKit
class NotificationContentViewModel: SettingsTableViewModel<NoNav, NotificationSettingsViewModel.Section, Preferences.NotificationPreviewType> {
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<NoNav, NotificationSe
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableSettingsData: ObservableData = ValueObservation
.trackingConstantRegion { [weak self] db -> [SectionModel] in
.trackingConstantRegion { [storage] db -> [SectionModel] in
let currentSelection: Preferences.NotificationPreviewType? = db[.preferencesNotificationPreviewType]
.defaulting(to: .defaultPreviewType)
@ -48,7 +56,7 @@ class NotificationContentViewModel: SettingsTableViewModel<NoNav, NotificationSe
storedSelection: (currentSelection == previewType),
shouldAutoSave: true,
selectValue: {
Storage.shared.write { db in
storage.write { db in
db[.preferencesNotificationPreviewType] = previewType
}
}
@ -59,7 +67,7 @@ class NotificationContentViewModel: SettingsTableViewModel<NoNav, NotificationSe
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
.publisher(in: storage)
// MARK: - Functions

View File

@ -35,38 +35,45 @@ public enum GarbageCollectionJob: JobExecutor {
.defaulting(to: Types.allCases)
let timestampNow: TimeInterval = Date().timeIntervalSince1970
/// Only do something if the job isn't the recurring one or it's been 23 hours since it last ran (23 hours so a user who opens the
/// Only do a full collection if the job isn't the recurring one or it's been 23 hours since it last ran (23 hours so a user who opens the
/// app at about the same time every day will trigger the garbage collection) - since this runs when the app becomes active we
/// want to prevent it running to frequently (the app becomes active if a system alert, the notification center or the control panel
/// are shown)
let lastGarbageCollection: Date = UserDefaults.standard[.lastGarbageCollection]
.defaulting(to: Date.distantPast)
guard
job.behaviour != .recurringOnActive ||
Date().timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60)
else {
deferred(job)
return
}
let finalTypesToCollection: Set<Types> = {
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<Interaction> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = 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<Job> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = 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<LinkPreview> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = 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<OpenGroup> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = 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<Capability> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = 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<BlindedIdLookup> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = 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<Contact> = TypedTableAlias()
let blindedIdLookup: TypedTableAlias<BlindedIdLookup> = 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<Attachment> = TypedTableAlias()
let quote: TypedTableAlias<Quote> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
@ -248,7 +255,7 @@ public enum GarbageCollectionJob: JobExecutor {
""")
}
if typesToCollect.contains(.orphanedProfiles) {
if finalTypesToCollection.contains(.orphanedProfiles) {
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
@ -297,7 +304,7 @@ public enum GarbageCollectionJob: JobExecutor {
var profileAvatarFilenames: Set<String> = []
/// 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<String> = (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)
}
}

View File

@ -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) }

View File

@ -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",

View File

@ -10,4 +10,9 @@ class MockGeneralCache: Mock<GeneralCacheType>, GeneralCacheType {
get { return accept() as? String }
set { accept(args: [newValue]) }
}
var recentReactionTimestamps: [Int64] {
get { return (accept() as? [Int64] ?? []) }
set { accept(args: [newValue]) }
}
}

View File

@ -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<AnyCancellable>!
// 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)
// )
// }
//}

View File

@ -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<AnyCancellable>!
// 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<Void, Never> = 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)
//}

View File

@ -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.
}
}
}

View File

@ -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)
)
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -141,6 +141,9 @@ public enum ThemeValue {
case conversationButton_swipeSecondary
case conversationButton_swipeTertiary
// InputButton
case inputButton_background
// Call
case callAccept_background
case callDecline_background