mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge branch 'theming' into ipad-landscape-support
This commit is contained in:
commit
671c5f6ada
60 changed files with 1292 additions and 1274 deletions
|
@ -573,6 +573,13 @@
|
|||
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; };
|
||||
FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */; };
|
||||
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; };
|
||||
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
|
||||
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
|
||||
FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; };
|
||||
FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; };
|
||||
FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; };
|
||||
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; };
|
||||
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; };
|
||||
FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7552553A3AB00C340D1 /* VisibleMessage+Quote.swift */; };
|
||||
FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5FB2554B0A000555489 /* MessageReceiver.swift */; };
|
||||
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF224255B6D5D007E1867 /* SignalAttachment.swift */; };
|
||||
|
@ -599,6 +606,11 @@
|
|||
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
|
||||
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
|
||||
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
|
||||
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
|
||||
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
|
||||
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
|
||||
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
|
||||
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
|
||||
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; };
|
||||
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */; };
|
||||
FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */; };
|
||||
|
@ -1678,8 +1690,10 @@
|
|||
FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
|
||||
FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUKLegacy.swift; sourceTree = "<group>"; };
|
||||
FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
|
||||
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
|
||||
FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = "<group>"; };
|
||||
FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+ClassicDark.swift"; sourceTree = "<group>"; };
|
||||
FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Colors.swift"; sourceTree = "<group>"; };
|
||||
|
@ -3374,7 +3388,7 @@
|
|||
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
|
||||
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */,
|
||||
C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */,
|
||||
FD83B9BC27CF2215005E1583 /* SharedTest */,
|
||||
FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */,
|
||||
FD71160A28D00BAE00B47552 /* SessionTests */,
|
||||
FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */,
|
||||
FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */,
|
||||
|
@ -3897,15 +3911,18 @@
|
|||
path = General;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD83B9BC27CF2215005E1583 /* SharedTest */ = {
|
||||
FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FDC290A527D860CE005DAE71 /* Mock.swift */,
|
||||
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */,
|
||||
FD83B9BD27CF2243005E1583 /* TestConstants.swift */,
|
||||
FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */,
|
||||
FD078E4727E02561000769AF /* CommonMockedExtensions.swift */,
|
||||
FD23EA6028ED0B260058676E /* CombineExtensions.swift */,
|
||||
FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */,
|
||||
);
|
||||
path = SharedTest;
|
||||
path = _SharedTestUtilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD83B9C127CF33EE005E1583 /* Models */ = {
|
||||
|
@ -4052,7 +4069,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
FDC438BC27BB2AB400C60D73 /* Mockable.swift */,
|
||||
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */,
|
||||
FD859EF327C2F49200510D0C /* MockSodium.swift */,
|
||||
FD3C906E27E43E8700CD579F /* MockBox.swift */,
|
||||
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */,
|
||||
|
@ -5752,8 +5768,15 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */,
|
||||
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */,
|
||||
FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */,
|
||||
FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */,
|
||||
FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */,
|
||||
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */,
|
||||
FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */,
|
||||
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */,
|
||||
FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */,
|
||||
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -5761,10 +5784,13 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */,
|
||||
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
|
||||
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
|
||||
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
|
||||
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
|
||||
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
|
||||
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
|
||||
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
|
||||
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
|
||||
);
|
||||
|
@ -5789,6 +5815,7 @@
|
|||
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
|
||||
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
|
||||
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */,
|
||||
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */,
|
||||
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
|
||||
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
|
||||
FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */,
|
||||
|
@ -5800,6 +5827,7 @@
|
|||
FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */,
|
||||
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */,
|
||||
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */,
|
||||
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */,
|
||||
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
|
||||
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
|
||||
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
|
||||
|
@ -6004,7 +6032,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 375;
|
||||
CURRENT_PROJECT_VERSION = 376;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6029,7 +6057,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.1;
|
||||
MARKETING_VERSION = 2.2.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -6077,7 +6105,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 375;
|
||||
CURRENT_PROJECT_VERSION = 376;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6107,7 +6135,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.1;
|
||||
MARKETING_VERSION = 2.2.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -6143,7 +6171,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 375;
|
||||
CURRENT_PROJECT_VERSION = 376;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6166,7 +6194,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.1;
|
||||
MARKETING_VERSION = 2.2.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||
|
@ -6217,7 +6245,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 375;
|
||||
CURRENT_PROJECT_VERSION = 376;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6245,7 +6273,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.1;
|
||||
MARKETING_VERSION = 2.2.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||
|
@ -7145,7 +7173,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 375;
|
||||
CURRENT_PROJECT_VERSION = 376;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7184,7 +7212,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.1.1;
|
||||
MARKETING_VERSION = 2.2.0;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
|
@ -7217,7 +7245,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 375;
|
||||
CURRENT_PROJECT_VERSION = 376;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7256,7 +7284,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.1.1;
|
||||
MARKETING_VERSION = 2.2.0;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = Session;
|
||||
|
|
|
@ -118,6 +118,18 @@
|
|||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES"
|
||||
testExecutionOrdering = "random">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "FD71160828D00BAE00B47552"
|
||||
BuildableName = "SessionTests.xctest"
|
||||
BlueprintName = "SessionTests"
|
||||
ReferencedContainer = "container:Session.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES"
|
||||
|
@ -142,18 +154,6 @@
|
|||
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
|
||||
|
|
|
@ -46,27 +46,18 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var fadeView: UIView = {
|
||||
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64)
|
||||
|
||||
let result = UIView()
|
||||
var frame = UIScreen.main.bounds
|
||||
frame.size.height = height
|
||||
|
||||
let layer = CAGradientLayer()
|
||||
layer.frame = frame
|
||||
result.layer.insertSublayer(layer, at: 0)
|
||||
private lazy var fadeView: GradientView = {
|
||||
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top)
|
||||
.map { $0 + Values.veryLargeSpacing })
|
||||
.defaulting(to: 64)
|
||||
|
||||
let result: GradientView = GradientView()
|
||||
result.themeBackgroundGradient = [
|
||||
.value(.backgroundPrimary, alpha: 0.4),
|
||||
.value(.backgroundPrimary, alpha: 0)
|
||||
]
|
||||
result.set(.height, to: height)
|
||||
|
||||
ThemeManager.onThemeChange(observer: result) { [weak layer] theme, _ in
|
||||
guard let backgroundPrimary: UIColor = theme.color(for: .backgroundPrimary) else { return }
|
||||
|
||||
layer?.colors = [
|
||||
backgroundPrimary.withAlphaComponent(0.4).cgColor,
|
||||
backgroundPrimary.withAlphaComponent(0).cgColor
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
|
|
|
@ -26,27 +26,18 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var fadeView: UIView = {
|
||||
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64)
|
||||
|
||||
let result = UIView()
|
||||
var frame = UIScreen.main.bounds
|
||||
frame.size.height = height
|
||||
|
||||
let layer = CAGradientLayer()
|
||||
layer.frame = frame
|
||||
result.layer.insertSublayer(layer, at: 0)
|
||||
private lazy var fadeView: GradientView = {
|
||||
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top)
|
||||
.map { $0 + Values.veryLargeSpacing })
|
||||
.defaulting(to: 64)
|
||||
|
||||
let result: GradientView = GradientView()
|
||||
result.themeBackgroundGradient = [
|
||||
.value(.backgroundPrimary, alpha: 0.4),
|
||||
.value(.backgroundPrimary, alpha: 0)
|
||||
]
|
||||
result.set(.height, to: height)
|
||||
|
||||
ThemeManager.onThemeChange(observer: result) { [weak layer] theme, _ in
|
||||
guard let backgroundPrimary: UIColor = theme.color(for: .backgroundPrimary) else { return }
|
||||
|
||||
layer?.colors = [
|
||||
backgroundPrimary.withAlphaComponent(0.4).cgColor,
|
||||
backgroundPrimary.withAlphaComponent(0).cgColor
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
|
|
|
@ -108,12 +108,15 @@ extension ContextMenuVC {
|
|||
currentThreadIsMessageRequest: Bool,
|
||||
delegate: ContextMenuActionDelegate?
|
||||
) -> [Action]? {
|
||||
// No context items for info messages
|
||||
guard cellViewModel.variant != .standardIncomingDeleted else {
|
||||
return [ Action.delete(cellViewModel, delegate) ]
|
||||
}
|
||||
guard cellViewModel.variant == .standardOutgoing || cellViewModel.variant == .standardIncoming else {
|
||||
return nil
|
||||
switch cellViewModel.variant {
|
||||
case .standardIncomingDeleted, .infoCall,
|
||||
.infoScreenshotNotification, .infoMediaSavedNotification,
|
||||
.infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
|
||||
.infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
|
||||
// Let the user delete info messages and unsent messages
|
||||
return [ Action.delete(cellViewModel, delegate) ]
|
||||
|
||||
case .standardOutgoing, .standardIncoming: break
|
||||
}
|
||||
|
||||
let canReply: Bool = (
|
||||
|
|
|
@ -225,7 +225,8 @@ final class ContextMenuVC: UIViewController {
|
|||
menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX)
|
||||
emojiBar.pin(.left, to: .left, of: view, withInset: targetFrame.minX)
|
||||
|
||||
default: break // Should never occur
|
||||
default: // Should generally only be the 'delete' action
|
||||
menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX)
|
||||
}
|
||||
|
||||
// Tap gesture
|
||||
|
|
|
@ -766,8 +766,9 @@ extension ConversationVC:
|
|||
let index = self.viewModel.interactionData[sectionIndex]
|
||||
.elements
|
||||
.firstIndex(of: cellViewModel),
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? VisibleMessageCell,
|
||||
let snapshot = cell.snContentView.snapshotView(afterScreenUpdates: false),
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? MessageCell,
|
||||
let contextSnapshotView: UIView = cell.contextSnapshotView,
|
||||
let snapshot = contextSnapshotView.snapshotView(afterScreenUpdates: false),
|
||||
contextMenuWindow == nil,
|
||||
let actions: [ContextMenuVC.Action] = ContextMenuVC.actions(
|
||||
for: cellViewModel,
|
||||
|
@ -789,7 +790,7 @@ extension ConversationVC:
|
|||
self.contextMenuWindow = ContextMenuWindow()
|
||||
self.contextMenuVC = ContextMenuVC(
|
||||
snapshot: snapshot,
|
||||
frame: cell.convert(cell.snContentView.frame, to: keyWindow),
|
||||
frame: contextSnapshotView.convert(contextSnapshotView.bounds, to: keyWindow),
|
||||
cellViewModel: cellViewModel,
|
||||
actions: actions
|
||||
) { [weak self] in
|
||||
|
@ -1218,7 +1219,18 @@ extension ConversationVC:
|
|||
guard
|
||||
recentReactionTimestamps.count < 20 ||
|
||||
(sentTimestamp - (recentReactionTimestamps.first ?? sentTimestamp)) > (60 * 1000)
|
||||
else { return }
|
||||
else {
|
||||
let toastController: ToastController = ToastController(
|
||||
text: "EMOJI_REACTS_RATE_LIMIT_TOAST".localized(),
|
||||
background: .backgroundSecondary
|
||||
)
|
||||
toastController.presentToastView(
|
||||
fromBottomOfView: self.view,
|
||||
inset: (snInputView.bounds.height + Values.largeSpacing),
|
||||
duration: .milliseconds(2500)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
General.cache.mutate {
|
||||
$0.recentReactionTimestamps = Array($0.recentReactionTimestamps
|
||||
|
@ -1593,17 +1605,22 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
func delete(_ cellViewModel: MessageViewModel) {
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
guard cellViewModel.variant != .standardIncomingDeleted else {
|
||||
Storage.shared.writeAsync { db in
|
||||
_ = try Interaction
|
||||
.filter(id: cellViewModel.id)
|
||||
.deleteAll(db)
|
||||
}
|
||||
return
|
||||
}
|
||||
guard cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardOutgoing else {
|
||||
return
|
||||
switch cellViewModel.variant {
|
||||
case .standardIncomingDeleted, .infoCall,
|
||||
.infoScreenshotNotification, .infoMediaSavedNotification,
|
||||
.infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
|
||||
.infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
|
||||
// Info messages and unsent messages should just trigger a local
|
||||
// deletion (they are created as side effects so we wouldn't be
|
||||
// able to delete them for all participants anyway)
|
||||
Storage.shared.writeAsync { db in
|
||||
_ = try Interaction
|
||||
.filter(id: cellViewModel.id)
|
||||
.deleteAll(db)
|
||||
}
|
||||
return
|
||||
|
||||
case .standardOutgoing, .standardIncoming: break
|
||||
}
|
||||
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
|
|
|
@ -713,10 +713,23 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
let newSectionIndex: Int = updatedData.firstIndex(where: { $0.model == .messages }),
|
||||
let newFirstItemIndex: Int = updatedData[newSectionIndex].elements
|
||||
.firstIndex(where: { item -> Bool in
|
||||
item.id == self.viewModel.interactionData[oldSectionIndex].elements.first?.id
|
||||
// Since the first item is probably a `DateHeaderCell` (which would likely
|
||||
// be removed when inserting items above it) we check if the id matches
|
||||
// either the first or second item
|
||||
let messages: [MessageViewModel] = self.viewModel
|
||||
.interactionData[oldSectionIndex]
|
||||
.elements
|
||||
|
||||
return (
|
||||
item.id == messages[safe: 0]?.id ||
|
||||
item.id == messages[safe: 1]?.id
|
||||
)
|
||||
}),
|
||||
let firstVisibleIndexPath: IndexPath = self.tableView.indexPathsForVisibleRows?
|
||||
.filter({ $0.section == oldSectionIndex })
|
||||
.filter({
|
||||
$0.section == oldSectionIndex &&
|
||||
self.viewModel.interactionData[$0.section].elements[$0.row].cellType != .dateHeader
|
||||
})
|
||||
.sorted()
|
||||
.first,
|
||||
let newVisibleIndex: Int = updatedData[newSectionIndex].elements
|
||||
|
@ -730,7 +743,9 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
return ItemChangeInfo(
|
||||
isInsertAtTop: (
|
||||
newSectionIndex > oldSectionIndex ||
|
||||
newFirstItemIndex > 0
|
||||
// Note: Using `1` here instead of `0` as the first item will generally
|
||||
// be a `DateHeaderCell` instead of a message
|
||||
newFirstItemIndex > 1
|
||||
),
|
||||
firstIndexIsVisible: (firstVisibleIndexPath.row == 0),
|
||||
visibleIndexPath: IndexPath(row: newVisibleIndex, section: newSectionIndex),
|
||||
|
|
|
@ -9,14 +9,18 @@ final class CallMessageCell: MessageCell {
|
|||
private static let inset = Values.mediumSpacing
|
||||
private static let margin = UIScreen.main.bounds.width * 0.1
|
||||
|
||||
private lazy var iconImageViewWidthConstraint = iconImageView.set(.width, to: 0)
|
||||
private lazy var iconImageViewHeightConstraint = iconImageView.set(.height, to: 0)
|
||||
private var isHandlingLongPress: Bool = false
|
||||
|
||||
private lazy var infoImageViewWidthConstraint = infoImageView.set(.width, to: 0)
|
||||
private lazy var infoImageViewHeightConstraint = infoImageView.set(.height, to: 0)
|
||||
override var contextSnapshotView: UIView? { return container }
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var topConstraint: NSLayoutConstraint = container.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset)
|
||||
private lazy var iconImageViewWidthConstraint: NSLayoutConstraint = iconImageView.set(.width, to: 0)
|
||||
private lazy var iconImageViewHeightConstraint: NSLayoutConstraint = iconImageView.set(.height, to: 0)
|
||||
private lazy var infoImageViewWidthConstraint: NSLayoutConstraint = infoImageView.set(.width, to: 0)
|
||||
private lazy var infoImageViewHeightConstraint: NSLayoutConstraint = infoImageView.set(.height, to: 0)
|
||||
|
||||
private lazy var iconImageView: UIImageView = UIImageView()
|
||||
private lazy var infoImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(
|
||||
|
@ -28,15 +32,6 @@ final class CallMessageCell: MessageCell {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var timestampLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var label: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
|
@ -80,15 +75,6 @@ final class CallMessageCell: MessageCell {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var stackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView(arrangedSubviews: [ timestampLabel, container ])
|
||||
result.axis = .vertical
|
||||
result.alignment = .center
|
||||
result.spacing = Values.smallSpacing
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func setUpViewHierarchy() {
|
||||
|
@ -96,16 +82,18 @@ final class CallMessageCell: MessageCell {
|
|||
|
||||
iconImageViewWidthConstraint.isActive = true
|
||||
iconImageViewHeightConstraint.isActive = true
|
||||
addSubview(stackView)
|
||||
addSubview(container)
|
||||
|
||||
container.autoPinWidthToSuperview()
|
||||
stackView.pin(.left, to: .left, of: self, withInset: CallMessageCell.margin)
|
||||
stackView.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset)
|
||||
stackView.pin(.right, to: .right, of: self, withInset: -CallMessageCell.margin)
|
||||
stackView.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset)
|
||||
topConstraint.isActive = true
|
||||
container.pin(.left, to: .left, of: self, withInset: CallMessageCell.margin)
|
||||
container.pin(.right, to: .right, of: self, withInset: -CallMessageCell.margin)
|
||||
container.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset)
|
||||
}
|
||||
|
||||
override func setUpGestureRecognizers() {
|
||||
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
|
||||
addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
tapGestureRecognizer.numberOfTapsRequired = 1
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
|
@ -130,6 +118,7 @@ final class CallMessageCell: MessageCell {
|
|||
else { return }
|
||||
|
||||
self.viewModel = cellViewModel
|
||||
self.topConstraint.constant = (cellViewModel.shouldShowDateHeader ? 0 : CallMessageCell.inset)
|
||||
|
||||
iconImageView.image = {
|
||||
switch messageInfo.state {
|
||||
|
@ -157,12 +146,24 @@ final class CallMessageCell: MessageCell {
|
|||
infoImageViewHeightConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0)
|
||||
|
||||
label.text = cellViewModel.body
|
||||
timestampLabel.text = cellViewModel.dateForUI.formattedForDisplay
|
||||
}
|
||||
|
||||
override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) {
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) {
|
||||
isHandlingLongPress = false
|
||||
return
|
||||
}
|
||||
guard !isHandlingLongPress, let cellViewModel: MessageViewModel = self.viewModel else { return }
|
||||
|
||||
delegate?.handleItemLongPressed(cellViewModel)
|
||||
isHandlingLongPress = true
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard
|
||||
let cellViewModel: MessageViewModel = self.viewModel,
|
||||
|
|
|
@ -76,15 +76,11 @@ final class DocumentView: UIView {
|
|||
stackView.axis = .horizontal
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
stackView.alignment = .center
|
||||
stackView.layoutMargins = UIEdgeInsets(
|
||||
top: Values.smallSpacing,
|
||||
leading: Values.mediumSpacing,
|
||||
bottom: Values.smallSpacing,
|
||||
trailing: Values.mediumSpacing
|
||||
)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
addSubview(stackView)
|
||||
stackView.pin(to: self)
|
||||
stackView.pin(.top, to: .top, of: self, withInset: Values.smallSpacing)
|
||||
stackView.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing)
|
||||
stackView.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing)
|
||||
stackView.pin(.bottom, to: .bottom, of: self, withInset: -Values.smallSpacing)
|
||||
|
||||
imageBackgroundView.pin(.top, to: .top, of: self)
|
||||
imageBackgroundView.pin(.leading, to: .leading, of: self)
|
||||
|
|
|
@ -2,11 +2,17 @@
|
|||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class ReactionContainerView: UIView {
|
||||
var showingAllReactions = false
|
||||
private var showNumbers = true
|
||||
private static let arrowSize: CGSize = CGSize(width: 15, height: 13)
|
||||
private static let arrowSpacing: CGFloat = Values.verySmallSpacing
|
||||
|
||||
private var maxWidth: CGFloat = 0
|
||||
private var collapsedCount: Int = 0
|
||||
private var showingAllReactions: Bool = false
|
||||
private var showNumbers: Bool = true
|
||||
private var maxEmojisPerLine = UIDevice.current.isIPad ? 10 : (isIPhone6OrSmaller ? 5 : 6)
|
||||
private var oldSize: CGSize = .zero
|
||||
|
||||
|
@ -15,6 +21,16 @@ final class ReactionContainerView: UIView {
|
|||
|
||||
// MARK: - UI
|
||||
|
||||
private var collapseTextLabelRightConstraint: NSLayoutConstraint?
|
||||
|
||||
private let dummyReactionButton: ReactionButton = ReactionButton(
|
||||
viewModel: ReactionViewModel(
|
||||
emoji: EmojiWithSkinTones(baseEmoji: .a, skinTones: nil),
|
||||
number: 0,
|
||||
showBorder: false
|
||||
)
|
||||
)
|
||||
|
||||
private lazy var mainStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView(arrangedSubviews: [ reactionContainerView, collapseButton ])
|
||||
result.axis = .vertical
|
||||
|
@ -24,6 +40,8 @@ final class ReactionContainerView: UIView {
|
|||
return result
|
||||
}()
|
||||
|
||||
var expandButton: ExpandingReactionButton?
|
||||
|
||||
private lazy var reactionContainerView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.axis = .vertical
|
||||
|
@ -33,34 +51,36 @@ final class ReactionContainerView: UIView {
|
|||
return result
|
||||
}()
|
||||
|
||||
var expandButton: ExpandingReactionButton?
|
||||
|
||||
var collapseButton: UIStackView = {
|
||||
let arrow = UIImageView(
|
||||
lazy var collapseButton: UIView = {
|
||||
let arrow: UIImageView = UIImageView(
|
||||
image: UIImage(named: "ic_chevron_up")?
|
||||
.resizedImage(to: CGSize(width: 15, height: 13))?
|
||||
.resizedImage(to: ReactionContainerView.arrowSize)?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
)
|
||||
arrow.themeTintColor = .textPrimary
|
||||
|
||||
let textLabel: UILabel = UILabel()
|
||||
textLabel.setContentHuggingPriority(.required, for: .vertical)
|
||||
textLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
textLabel.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
textLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
textLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
textLabel.text = "EMOJI_REACTS_SHOW_LESS".localized()
|
||||
textLabel.themeTextColor = .textPrimary
|
||||
|
||||
let leftSpacer: UIView = UIView.hStretchingSpacer()
|
||||
let rightSpacer: UIView = UIView.hStretchingSpacer()
|
||||
let result: UIStackView = UIStackView(arrangedSubviews: [
|
||||
leftSpacer,
|
||||
arrow,
|
||||
textLabel,
|
||||
rightSpacer
|
||||
])
|
||||
result.isLayoutMarginsRelativeArrangement = true
|
||||
result.spacing = Values.verySmallSpacing
|
||||
result.alignment = .center
|
||||
let result: UIView = UIView()
|
||||
result.isHidden = true
|
||||
rightSpacer.set(.width, to: .width, of: leftSpacer)
|
||||
result.addSubview(arrow)
|
||||
result.addSubview(textLabel)
|
||||
|
||||
arrow.pin(.top, to: .top, of: result)
|
||||
arrow.pin(.leading, to: .leading, of: result)
|
||||
arrow.pin(.bottom, to: .bottom, of: result)
|
||||
|
||||
textLabel.pin(.top, to: .top, of: result)
|
||||
textLabel.pin(.leading, to: .trailing, of: arrow, withInset: ReactionContainerView.arrowSpacing)
|
||||
collapseTextLabelRightConstraint = textLabel.pin(.trailing, to: .trailing, of: result)
|
||||
textLabel.pin(.bottom, to: .bottom, of: result)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -84,123 +104,181 @@ final class ReactionContainerView: UIView {
|
|||
addSubview(mainStackView)
|
||||
|
||||
mainStackView.pin(to: self)
|
||||
collapseButton.set(.width, to: .width, of: mainStackView)
|
||||
reactionContainerView.set(.width, to: .width, of: mainStackView)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
// Note: We update the 'collapseButton.layoutMargins' to try to make the "show less"
|
||||
// Note: We update the 'collapseTextLabelRightConstraint' to try to make the "show less"
|
||||
// button appear horizontally centered (if we don't do this it gets offset to one side)
|
||||
guard frame != CGRect.zero, frame.size != oldSize else { return }
|
||||
|
||||
collapseButton.layoutMargins = UIEdgeInsets(
|
||||
top: 0,
|
||||
leading: -frame.minX,
|
||||
bottom: 0,
|
||||
trailing: -((superview?.frame.width ?? 0) - frame.maxX)
|
||||
)
|
||||
var targetSuperview: UIView? = {
|
||||
var result: UIView? = self.superview
|
||||
|
||||
while result != nil, result?.isKind(of: UITableViewCell.self) != true {
|
||||
result = result?.superview
|
||||
}
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
if let targetSuperview: UIView = targetSuperview {
|
||||
let parentWidth: CGFloat = targetSuperview.bounds.width
|
||||
let frameInParent: CGRect = targetSuperview.convert(self.bounds, from: self)
|
||||
let centeredWidth: CGFloat = (parentWidth - (frameInParent.minX * 2))
|
||||
let diff: CGFloat = (frameInParent.width - centeredWidth)
|
||||
collapseTextLabelRightConstraint?.constant = -(
|
||||
diff +
|
||||
((ReactionContainerView.arrowSize.width + ReactionContainerView.arrowSpacing) / 2)
|
||||
)
|
||||
}
|
||||
|
||||
oldSize = frame.size
|
||||
}
|
||||
|
||||
public func update(_ reactions: [ReactionViewModel], showNumbers: Bool) {
|
||||
public func update(
|
||||
_ reactions: [ReactionViewModel],
|
||||
maxWidth: CGFloat,
|
||||
showingAllReactions: Bool,
|
||||
showNumbers: Bool
|
||||
) {
|
||||
self.reactions = reactions
|
||||
self.maxWidth = maxWidth
|
||||
self.collapsedCount = {
|
||||
var numReactions: Int = 0
|
||||
var runningWidth: CGFloat = 0
|
||||
let estimatedExpandingButtonWidth: CGFloat = 52
|
||||
let itemSpacing: CGFloat = self.reactionContainerView.spacing
|
||||
|
||||
for reaction in reactions {
|
||||
let reactionViewWidth: CGFloat = dummyReactionButton
|
||||
.updating(with: reaction, showNumber: showNumbers)
|
||||
.systemLayoutSizeFitting(CGSize(width: maxWidth, height: 9999))
|
||||
.width
|
||||
let estimatedFullWidth: CGFloat = (
|
||||
runningWidth +
|
||||
(reactionViewWidth + itemSpacing) +
|
||||
estimatedExpandingButtonWidth
|
||||
)
|
||||
|
||||
if estimatedFullWidth >= maxWidth {
|
||||
break
|
||||
}
|
||||
|
||||
runningWidth += (reactionViewWidth + itemSpacing)
|
||||
numReactions += 1
|
||||
}
|
||||
|
||||
return numReactions
|
||||
}()
|
||||
self.showNumbers = showNumbers
|
||||
self.reactionViews = []
|
||||
self.reactionContainerView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
prepareForUpdate()
|
||||
|
||||
if showingAllReactions {
|
||||
updateAllReactions()
|
||||
// Generate the lines of reactions (if the 'collapsedCount' matches the total number of
|
||||
// reactions then just show them app)
|
||||
if showingAllReactions || self.collapsedCount >= reactions.count {
|
||||
self.updateAllReactions(reactions, maxWidth: maxWidth, showNumbers: showNumbers)
|
||||
}
|
||||
else {
|
||||
updateCollapsedReactions(reactions)
|
||||
self.updateCollapsedReactions(reactions, maxWidth: maxWidth, showNumbers: showNumbers)
|
||||
}
|
||||
|
||||
// Just in case we couldn't show everything for some reason update this based on the
|
||||
// internal logic
|
||||
self.collapseButton.isHidden = (self.reactionContainerView.arrangedSubviews.count <= 1)
|
||||
self.showingAllReactions = !self.collapseButton.isHidden
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
|
||||
private func updateCollapsedReactions(_ reactions: [ReactionViewModel]) {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = Values.smallSpacing
|
||||
stackView.alignment = .center
|
||||
private func createLineStackView() -> UIStackView {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.spacing = Values.smallSpacing
|
||||
result.alignment = .center
|
||||
result.set(.height, to: ReactionButton.height)
|
||||
|
||||
var displayedReactions: [ReactionViewModel]
|
||||
var expandButtonReactions: [EmojiWithSkinTones]
|
||||
return result
|
||||
}
|
||||
|
||||
private func updateCollapsedReactions(
|
||||
_ reactions: [ReactionViewModel],
|
||||
maxWidth: CGFloat,
|
||||
showNumbers: Bool
|
||||
) {
|
||||
guard !reactions.isEmpty else { return }
|
||||
|
||||
if reactions.count > maxEmojisPerLine {
|
||||
displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)])
|
||||
expandButtonReactions = Array(reactions[(maxEmojisPerLine - 2)...maxEmojisPerLine])
|
||||
.map { $0.emoji }
|
||||
}
|
||||
else {
|
||||
displayedReactions = reactions
|
||||
expandButtonReactions = []
|
||||
}
|
||||
let maxSize: CGSize = CGSize(width: maxWidth, height: 9999)
|
||||
let stackView: UIStackView = createLineStackView()
|
||||
let displayedReactions: [ReactionViewModel] = Array(reactions.prefix(upTo: self.collapsedCount))
|
||||
let expandButtonReactions: [EmojiWithSkinTones] = reactions
|
||||
.suffix(from: self.collapsedCount)
|
||||
.prefix(3)
|
||||
.map { $0.emoji }
|
||||
|
||||
for reaction in displayedReactions {
|
||||
let reactionView = ReactionButton(viewModel: reaction, showNumber: showNumbers)
|
||||
let reactionViewWidth: CGFloat = reactionView.systemLayoutSizeFitting(maxSize).width
|
||||
stackView.addArrangedSubview(reactionView)
|
||||
reactionViews.append(reactionView)
|
||||
reactionView.set(.width, to: reactionViewWidth)
|
||||
}
|
||||
|
||||
if expandButtonReactions.count > 0 {
|
||||
let expandButton: ExpandingReactionButton = ExpandingReactionButton(emojis: expandButtonReactions)
|
||||
stackView.addArrangedSubview(expandButton)
|
||||
self.expandButton = {
|
||||
guard !expandButtonReactions.isEmpty else { return nil }
|
||||
|
||||
let result: ExpandingReactionButton = ExpandingReactionButton(emojis: expandButtonReactions)
|
||||
stackView.addArrangedSubview(result)
|
||||
|
||||
self.expandButton = expandButton
|
||||
}
|
||||
else {
|
||||
expandButton = nil
|
||||
}
|
||||
return result
|
||||
}()
|
||||
|
||||
reactionContainerView.addArrangedSubview(stackView)
|
||||
}
|
||||
|
||||
private func updateAllReactions() {
|
||||
var reactions = self.reactions
|
||||
var numberOfLines = 0
|
||||
private func updateAllReactions(
|
||||
_ reactions: [ReactionViewModel],
|
||||
maxWidth: CGFloat,
|
||||
showNumbers: Bool
|
||||
) {
|
||||
guard !reactions.isEmpty else { return }
|
||||
|
||||
while reactions.count > 0 {
|
||||
var line: [ReactionViewModel] = []
|
||||
let maxSize: CGSize = CGSize(width: maxWidth, height: 9999)
|
||||
var lineStackView: UIStackView = createLineStackView()
|
||||
reactionContainerView.addArrangedSubview(lineStackView)
|
||||
|
||||
for reaction in self.reactions {
|
||||
let reactionView: ReactionButton = ReactionButton(viewModel: reaction, showNumber: showNumbers)
|
||||
let reactionViewWidth: CGFloat = reactionView.systemLayoutSizeFitting(maxSize).width
|
||||
reactionViews.append(reactionView)
|
||||
|
||||
while reactions.count > 0 && line.count < maxEmojisPerLine {
|
||||
line.append(reactions.removeFirst())
|
||||
// Check if we need to create a new line
|
||||
let stackViewWidth: CGFloat = (lineStackView.arrangedSubviews.isEmpty ?
|
||||
0 :
|
||||
lineStackView.systemLayoutSizeFitting(maxSize).width
|
||||
)
|
||||
|
||||
if stackViewWidth + reactionViewWidth > maxWidth {
|
||||
lineStackView = createLineStackView()
|
||||
reactionContainerView.addArrangedSubview(lineStackView)
|
||||
}
|
||||
|
||||
updateCollapsedReactions(line)
|
||||
numberOfLines += 1
|
||||
lineStackView.addArrangedSubview(reactionView)
|
||||
reactionView.set(.width, to: reactionViewWidth)
|
||||
}
|
||||
|
||||
if numberOfLines > 1 {
|
||||
collapseButton.isHidden = false
|
||||
}
|
||||
else {
|
||||
showingAllReactions = false
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareForUpdate() {
|
||||
for subview in reactionContainerView.arrangedSubviews {
|
||||
reactionContainerView.removeArrangedSubview(subview)
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
|
||||
collapseButton.isHidden = true
|
||||
reactionViews = []
|
||||
}
|
||||
|
||||
public func showAllEmojis() {
|
||||
guard !showingAllReactions else { return }
|
||||
|
||||
showingAllReactions = true
|
||||
update(reactions, showNumbers: showNumbers)
|
||||
update(reactions, maxWidth: maxWidth, showingAllReactions: true, showNumbers: showNumbers)
|
||||
}
|
||||
|
||||
public func showLessEmojis() {
|
||||
guard showingAllReactions else { return }
|
||||
|
||||
showingAllReactions = false
|
||||
update(reactions, showNumbers: showNumbers)
|
||||
update(reactions, maxWidth: maxWidth, showingAllReactions: false, showNumbers: showNumbers)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -15,10 +15,29 @@ final class ReactionButton: UIView {
|
|||
|
||||
// MARK: - Settings
|
||||
|
||||
private var height: CGFloat = 22
|
||||
public static var height: CGFloat = 22
|
||||
private var fontSize: CGFloat = Values.verySmallFontSize
|
||||
private var spacing: CGFloat = Values.verySmallSpacing
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var emojiLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.setContentHuggingPriority(.required, for: .horizontal)
|
||||
result.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
result.font = .systemFont(ofSize: fontSize)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var numberLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .systemFont(ofSize: fontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(viewModel: ReactionViewModel, showNumber: Bool = true) {
|
||||
|
@ -28,6 +47,7 @@ final class ReactionButton: UIView {
|
|||
super.init(frame: CGRect.zero)
|
||||
|
||||
setUpViewHierarchy()
|
||||
update(with: viewModel, showNumber: showNumber)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -39,35 +59,45 @@ final class ReactionButton: UIView {
|
|||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
let emojiLabel: UILabel = UILabel()
|
||||
emojiLabel.font = .systemFont(ofSize: fontSize)
|
||||
emojiLabel.text = viewModel.emoji.rawValue
|
||||
|
||||
let stackView: UIStackView = UIStackView(arrangedSubviews: [ emojiLabel ])
|
||||
let stackView: UIStackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = spacing
|
||||
stackView.alignment = .center
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.smallSpacing, bottom: 0, right: Values.smallSpacing)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
addSubview(stackView)
|
||||
stackView.pin(to: self)
|
||||
stackView.pin(.top, to: .top, of: self)
|
||||
stackView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing)
|
||||
stackView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing)
|
||||
stackView.pin(.bottom, to: .bottom, of: self)
|
||||
|
||||
themeBorderColor = (viewModel.showBorder ? .primary : .clear)
|
||||
themeBackgroundColor = .messageBubble_incomingBackground
|
||||
layer.cornerRadius = (self.height / 2)
|
||||
layer.cornerRadius = (ReactionButton.height / 2)
|
||||
layer.borderWidth = 1 // Intentionally 1pt (instead of 'Values.separatorThickness')
|
||||
set(.height, to: self.height)
|
||||
set(.height, to: ReactionButton.height)
|
||||
|
||||
if showNumber || viewModel.number > 1 {
|
||||
let numberLabel = UILabel()
|
||||
numberLabel.font = .systemFont(ofSize: fontSize)
|
||||
numberLabel.text = (viewModel.number < 1000 ?
|
||||
"\(viewModel.number)" :
|
||||
String(format: "%.1f", Float(viewModel.number) / 1000) + "k"
|
||||
)
|
||||
numberLabel.themeTextColor = .textPrimary
|
||||
stackView.addArrangedSubview(numberLabel)
|
||||
numberLabel.isHidden = (!showNumber && viewModel.number <= 1)
|
||||
}
|
||||
|
||||
func update(with viewModel: ReactionViewModel, showNumber: Bool) {
|
||||
_ = updating(with: viewModel, showNumber: showNumber)
|
||||
}
|
||||
|
||||
func updating(with viewModel: ReactionViewModel, showNumber: Bool) -> ReactionButton {
|
||||
emojiLabel.text = viewModel.emoji.rawValue
|
||||
numberLabel.text = (viewModel.number < 1000 ?
|
||||
"\(viewModel.number)" :
|
||||
String(format: "%.1f", Float(viewModel.number) / 1000) + "k"
|
||||
)
|
||||
numberLabel.isHidden = (!showNumber && viewModel.number <= 1)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.setNeedsLayout()
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ final class InfoMessageCell: MessageCell {
|
|||
private static let iconSize: CGFloat = 16
|
||||
private static let inset = Values.mediumSpacing
|
||||
|
||||
private var isHandlingLongPress: Bool = false
|
||||
|
||||
override var contextSnapshotView: UIView? { return label }
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var iconImageViewWidthConstraint = iconImageView.set(.width, to: InfoMessageCell.iconSize)
|
||||
|
@ -49,6 +53,11 @@ final class InfoMessageCell: MessageCell {
|
|||
stackView.pin(.right, to: .right, of: self, withInset: -InfoMessageCell.inset)
|
||||
stackView.pin(.bottom, to: .bottom, of: self, withInset: -InfoMessageCell.inset)
|
||||
}
|
||||
|
||||
override func setUpGestureRecognizers() {
|
||||
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
|
||||
addGestureRecognizer(longPressRecognizer)
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
|
@ -90,4 +99,17 @@ final class InfoMessageCell: MessageCell {
|
|||
|
||||
override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) {
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) {
|
||||
isHandlingLongPress = false
|
||||
return
|
||||
}
|
||||
guard !isHandlingLongPress, let cellViewModel: MessageViewModel = self.viewModel else { return }
|
||||
|
||||
delegate?.handleItemLongPressed(cellViewModel)
|
||||
isHandlingLongPress = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,9 @@ public enum SwipeState {
|
|||
}
|
||||
|
||||
public class MessageCell: UITableViewCell {
|
||||
weak var delegate: MessageCellDelegate?
|
||||
var viewModel: MessageViewModel?
|
||||
weak var delegate: MessageCellDelegate?
|
||||
open var contextSnapshotView: UIView? { return nil }
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
var voiceMessageView: VoiceMessageView?
|
||||
var audioStateChanged: ((TimeInterval, Bool) -> ())?
|
||||
|
||||
override var contextSnapshotView: UIView? { return snContentView }
|
||||
|
||||
// Constraints
|
||||
private lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self)
|
||||
private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0)
|
||||
|
@ -25,13 +27,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing)
|
||||
private lazy var contentViewRightConstraint1 = snContentView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing)
|
||||
private lazy var contentViewRightConstraint2 = snContentView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize)
|
||||
private lazy var contentBottomConstraint = snContentView.bottomAnchor
|
||||
.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -1)
|
||||
|
||||
private lazy var reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: snContentView)
|
||||
private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: snContentView)
|
||||
|
||||
private lazy var messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: reactionContainerView, withInset: 0)
|
||||
private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize)
|
||||
private lazy var messageStatusImageViewHeightConstraint = messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize)
|
||||
private lazy var underBubbleStackViewIncomingLeadingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.leading, to: .leading, of: snContentView)
|
||||
private lazy var underBubbleStackViewIncomingTrailingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.trailing, to: .trailing, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing)
|
||||
private lazy var underBubbleStackViewOutgoingLeadingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.leading, to: .leading, of: self, withInset: VisibleMessageCell.contactThreadHSpacing)
|
||||
private lazy var underBubbleStackViewOutgoingTrailingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.trailing, to: .trailing, of: snContentView)
|
||||
private lazy var underBubbleStackViewNoHeightConstraint: NSLayoutConstraint = underBubbleStackView.set(.height, to: 0)
|
||||
|
||||
private lazy var timerViewOutgoingMessageConstraint = timerView.pin(.left, to: .left, of: self, withInset: VisibleMessageCell.contactThreadHSpacing)
|
||||
private lazy var timerViewIncomingMessageConstraint = timerView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing)
|
||||
|
@ -92,16 +95,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var reactionContainerView = ReactionContainerView()
|
||||
|
||||
internal lazy var messageStatusImageView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = VisibleMessageCell.messageStatusImageViewSize / 2
|
||||
result.layer.masksToBounds = true
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var replyButton: UIView = {
|
||||
let result = UIView()
|
||||
let size = VisibleMessageCell.replyButtonSize + 8
|
||||
|
@ -128,6 +121,27 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
}()
|
||||
|
||||
private lazy var timerView: OWSMessageTimerView = OWSMessageTimerView()
|
||||
|
||||
lazy var underBubbleStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [])
|
||||
result.setContentHuggingPriority(.required, for: .vertical)
|
||||
result.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
result.axis = .vertical
|
||||
result.spacing = Values.verySmallSpacing
|
||||
result.alignment = .trailing
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var reactionContainerView = ReactionContainerView()
|
||||
|
||||
internal lazy var messageStatusImageView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = VisibleMessageCell.messageStatusImageViewSize / 2
|
||||
result.layer.masksToBounds = true
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
|
@ -197,19 +211,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
timerView.center(.vertical, in: snContentView)
|
||||
timerViewOutgoingMessageConstraint.isActive = true
|
||||
|
||||
// Reaction view
|
||||
addSubview(reactionContainerView)
|
||||
reactionContainerView.pin(.top, to: .bottom, of: snContentView, withInset: Values.verySmallSpacing)
|
||||
reactionContainerViewLeftConstraint.isActive = true
|
||||
|
||||
// Message status image view
|
||||
addSubview(messageStatusImageView)
|
||||
messageStatusImageViewTopConstraint.isActive = true
|
||||
messageStatusImageView.pin(.right, to: .right, of: snContentView, withInset: -1)
|
||||
messageStatusImageView.pin(.bottom, to: .bottom, of: self, withInset: -1)
|
||||
messageStatusImageViewWidthConstraint.isActive = true
|
||||
messageStatusImageViewHeightConstraint.isActive = true
|
||||
|
||||
// Reply button
|
||||
addSubview(replyButton)
|
||||
replyButton.addSubview(replyIconImageView)
|
||||
|
@ -219,6 +220,20 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
// Remaining constraints
|
||||
authorLabel.pin(.left, to: .left, of: snContentView, withInset: VisibleMessageCell.authorLabelInset)
|
||||
|
||||
// Under bubble content
|
||||
addSubview(underBubbleStackView)
|
||||
underBubbleStackView.pin(.top, to: .bottom, of: snContentView, withInset: 5)
|
||||
underBubbleStackView.pin(.bottom, to: .bottom, of: self)
|
||||
|
||||
underBubbleStackView.addArrangedSubview(reactionContainerView)
|
||||
underBubbleStackView.addArrangedSubview(messageStatusImageView)
|
||||
|
||||
reactionContainerView.widthAnchor
|
||||
.constraint(lessThanOrEqualTo: underBubbleStackView.widthAnchor)
|
||||
.isActive = true
|
||||
messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize)
|
||||
messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize)
|
||||
}
|
||||
|
||||
override func setUpGestureRecognizers() {
|
||||
|
@ -298,12 +313,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
lastSearchText: lastSearchText
|
||||
)
|
||||
|
||||
// Reaction view
|
||||
reactionContainerView.isHidden = (cellViewModel.reactionInfo?.isEmpty == true)
|
||||
reactionContainerViewLeftConstraint.isActive = (cellViewModel.variant == .standardIncoming)
|
||||
reactionContainerViewRightConstraint.isActive = (cellViewModel.variant == .standardOutgoing)
|
||||
populateReaction(for: cellViewModel, showExpandedReactions: showExpandedReactions)
|
||||
|
||||
// Author label
|
||||
authorLabelTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0)
|
||||
authorLabel.isHidden = (cellViewModel.senderName == nil)
|
||||
|
@ -315,27 +324,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
let authorLabelSize = authorLabel.sizeThatFits(authorLabelAvailableSpace)
|
||||
authorLabelHeightConstraint.constant = (cellViewModel.senderName != nil ? authorLabelSize.height : 0)
|
||||
|
||||
// Message status image view
|
||||
let (image, tintColor) = cellViewModel.state.statusIconInfo(
|
||||
variant: cellViewModel.variant,
|
||||
hasAtLeastOneReadReceipt: cellViewModel.hasAtLeastOneReadReceipt
|
||||
)
|
||||
messageStatusImageView.image = image
|
||||
messageStatusImageView.themeTintColor = tintColor
|
||||
messageStatusImageView.isHidden = (
|
||||
cellViewModel.variant != .standardOutgoing ||
|
||||
cellViewModel.variant == .infoCall ||
|
||||
(
|
||||
cellViewModel.state == .sent &&
|
||||
!cellViewModel.isLast
|
||||
)
|
||||
)
|
||||
messageStatusImageViewTopConstraint.constant = (messageStatusImageView.isHidden ? 0 : 5)
|
||||
[ messageStatusImageViewWidthConstraint, messageStatusImageViewHeightConstraint ]
|
||||
.forEach {
|
||||
$0.constant = (messageStatusImageView.isHidden ? 0 : VisibleMessageCell.messageStatusImageViewSize)
|
||||
}
|
||||
|
||||
// Timer
|
||||
if
|
||||
let expiresStartedAtMs: Double = cellViewModel.expiresStartedAtMs,
|
||||
|
@ -367,6 +355,43 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
else {
|
||||
addGestureRecognizer(panGestureRecognizer)
|
||||
}
|
||||
|
||||
// Under bubble content
|
||||
underBubbleStackView.alignment = (cellViewModel.variant == .standardOutgoing ?
|
||||
.trailing :
|
||||
.leading
|
||||
)
|
||||
underBubbleStackViewIncomingLeadingConstraint.isActive = (cellViewModel.variant != .standardOutgoing)
|
||||
underBubbleStackViewIncomingTrailingConstraint.isActive = (cellViewModel.variant != .standardOutgoing)
|
||||
underBubbleStackViewOutgoingLeadingConstraint.isActive = (cellViewModel.variant == .standardOutgoing)
|
||||
underBubbleStackViewOutgoingTrailingConstraint.isActive = (cellViewModel.variant == .standardOutgoing)
|
||||
|
||||
// Reaction view
|
||||
reactionContainerView.isHidden = (cellViewModel.reactionInfo?.isEmpty != false)
|
||||
populateReaction(
|
||||
for: cellViewModel,
|
||||
maxWidth: VisibleMessageCell.getMaxWidth(
|
||||
for: cellViewModel,
|
||||
includingOppositeGutter: false
|
||||
),
|
||||
showExpandedReactions: showExpandedReactions
|
||||
)
|
||||
|
||||
// Message status image view
|
||||
let (image, tintColor) = cellViewModel.state.statusIconInfo(
|
||||
variant: cellViewModel.variant,
|
||||
hasAtLeastOneReadReceipt: cellViewModel.hasAtLeastOneReadReceipt
|
||||
)
|
||||
messageStatusImageView.image = image
|
||||
messageStatusImageView.themeTintColor = tintColor
|
||||
messageStatusImageView.isHidden = (
|
||||
cellViewModel.variant != .standardOutgoing ||
|
||||
cellViewModel.variant == .infoCall ||
|
||||
(
|
||||
cellViewModel.state == .sent &&
|
||||
!cellViewModel.isLast
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func populateContentView(
|
||||
|
@ -595,7 +620,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
private func populateReaction(for cellViewModel: MessageViewModel, showExpandedReactions: Bool) {
|
||||
private func populateReaction(
|
||||
for cellViewModel: MessageViewModel,
|
||||
maxWidth: CGFloat,
|
||||
showExpandedReactions: Bool
|
||||
) {
|
||||
let reactions: OrderedDictionary<EmojiWithSkinTones, ReactionViewModel> = (cellViewModel.reactionInfo ?? [])
|
||||
.reduce(into: OrderedDictionary()) { result, reactionInfo in
|
||||
guard let emoji: EmojiWithSkinTones = EmojiWithSkinTones(rawValue: reactionInfo.reaction.emoji) else {
|
||||
|
@ -626,9 +655,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
reactionContainerView.showingAllReactions = showExpandedReactions
|
||||
reactionContainerView.update(
|
||||
reactions.orderedValues,
|
||||
maxWidth: maxWidth,
|
||||
showingAllReactions: showExpandedReactions,
|
||||
showNumbers: (
|
||||
cellViewModel.threadVariant == .closedGroup ||
|
||||
cellViewModel.threadVariant == .openGroup
|
||||
|
@ -752,7 +782,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
if reactionContainerView.frame.contains(location) {
|
||||
if reactionContainerView.bounds.contains(reactionContainerView.convert(location, from: self)) {
|
||||
let convertedLocation = reactionContainerView.convert(location, from: self)
|
||||
|
||||
for reactionView in reactionContainerView.reactionViews {
|
||||
|
@ -774,7 +804,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
if profilePictureView.frame.contains(location), cellViewModel.shouldShowProfile {
|
||||
if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile {
|
||||
// For open groups only attempt to start a conversation if the author has a blinded id
|
||||
guard cellViewModel.threadVariant != .openGroup else {
|
||||
guard SessionId.Prefix(from: cellViewModel.authorId) == .blinded else { return }
|
||||
|
@ -793,11 +823,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
openGroupPublicKey: nil
|
||||
)
|
||||
}
|
||||
else if replyButton.alpha > 0 && replyButton.frame.contains(location) {
|
||||
else if replyButton.alpha > 0 && replyButton.bounds.contains(replyButton.convert(location, from: self)) {
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
reply()
|
||||
}
|
||||
else if reactionContainerView.frame.contains(location) {
|
||||
else if reactionContainerView.bounds.contains(reactionContainerView.convert(location, from: self)) {
|
||||
let convertedLocation = reactionContainerView.convert(location, from: self)
|
||||
|
||||
for reactionView in reactionContainerView.reactionViews {
|
||||
|
@ -813,7 +843,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
if let expandButton = reactionContainerView.expandButton, expandButton.frame.contains(convertedLocation) {
|
||||
if let expandButton = reactionContainerView.expandButton, expandButton.bounds.contains(expandButton.convert(location, from: self)) {
|
||||
reactionContainerView.showAllEmojis()
|
||||
delegate?.needsLayout(for: cellViewModel, expandingReactions: true)
|
||||
}
|
||||
|
@ -823,7 +853,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
delegate?.needsLayout(for: cellViewModel, expandingReactions: false)
|
||||
}
|
||||
}
|
||||
else if snContentView.frame.contains(location) {
|
||||
else if snContentView.bounds.contains(snContentView.convert(location, from: self)) {
|
||||
delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
@ -966,11 +996,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
static func getMaxWidth(for cellViewModel: MessageViewModel) -> CGFloat {
|
||||
static func getMaxWidth(for cellViewModel: MessageViewModel, includingOppositeGutter: Bool = true) -> CGFloat {
|
||||
let screen: CGRect = UIScreen.main.bounds
|
||||
let oppositeEdgePadding: CGFloat = (includingOppositeGutter ? gutterSize : contactThreadHSpacing)
|
||||
|
||||
switch cellViewModel.variant {
|
||||
case .standardOutgoing: return (screen.width - contactThreadHSpacing - gutterSize)
|
||||
case .standardOutgoing:
|
||||
return (screen.width - contactThreadHSpacing - oppositeEdgePadding)
|
||||
|
||||
case .standardIncoming, .standardIncomingDeleted:
|
||||
let isGroupThread = (
|
||||
cellViewModel.threadVariant == .openGroup ||
|
||||
|
@ -978,7 +1011,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
)
|
||||
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)
|
||||
|
||||
return (screen.width - leftGutterSize - gutterSize)
|
||||
return (screen.width - leftGutterSize - oppositeEdgePadding)
|
||||
|
||||
default: preconditionFailure()
|
||||
}
|
||||
|
|
|
@ -28,8 +28,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
|
|||
|
||||
// MARK: - Variables
|
||||
|
||||
private let storage: Storage
|
||||
private let scheduler: ValueObservationScheduler
|
||||
private let dependencies: Dependencies
|
||||
private let threadId: String
|
||||
private let config: DisappearingMessagesConfiguration
|
||||
private var storedSelection: TimeInterval
|
||||
|
@ -38,13 +37,11 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
|
|||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
storage: Storage = Storage.shared,
|
||||
scheduling scheduler: ValueObservationScheduler = Storage.defaultPublisherScheduler,
|
||||
dependencies: Dependencies = Dependencies(),
|
||||
threadId: String,
|
||||
config: DisappearingMessagesConfiguration
|
||||
) {
|
||||
self.storage = storage
|
||||
self.scheduler = scheduler
|
||||
self.dependencies = dependencies
|
||||
self.threadId = threadId
|
||||
self.config = config
|
||||
self.storedSelection = (config.isEnabled ? config.durationSeconds : 0)
|
||||
|
@ -133,7 +130,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
|
|||
]
|
||||
}
|
||||
.removeDuplicates()
|
||||
.publisher(in: storage, scheduling: scheduler)
|
||||
.publisher(in: dependencies.storage, scheduling: dependencies.scheduler)
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
|
@ -152,7 +149,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
|
|||
|
||||
guard self.config != updatedConfig else { return }
|
||||
|
||||
storage.writeAsync { db in
|
||||
dependencies.storage.writeAsync { db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
|
||||
// MARK: - Variables
|
||||
|
||||
private let dependencies: Dependencies
|
||||
private let threadId: String
|
||||
private let threadVariant: SessionThread.Variant
|
||||
private let didTriggerSearch: () -> ()
|
||||
|
@ -54,13 +55,19 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(threadId: String, threadVariant: SessionThread.Variant, didTriggerSearch: @escaping () -> ()) {
|
||||
init(
|
||||
dependencies: Dependencies = Dependencies(),
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
didTriggerSearch: @escaping () -> ()
|
||||
) {
|
||||
self.dependencies = dependencies
|
||||
self.threadId = threadId
|
||||
self.threadVariant = threadVariant
|
||||
self.didTriggerSearch = didTriggerSearch
|
||||
self.oldDisplayName = (threadVariant != .contact ?
|
||||
nil :
|
||||
Storage.shared.read { db in
|
||||
dependencies.storage.read { db in
|
||||
try Profile
|
||||
.filter(id: threadId)
|
||||
.select(.nickname)
|
||||
|
@ -73,55 +80,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
// MARK: - Navigation
|
||||
|
||||
lazy var navState: AnyPublisher<NavState, Never> = {
|
||||
Publishers
|
||||
.MergeMany(
|
||||
isEditing
|
||||
.filter { $0 }
|
||||
.map { _ in .editing }
|
||||
.eraseToAnyPublisher(),
|
||||
navItemTapped
|
||||
.filter { $0 == .edit }
|
||||
.map { _ in .editing }
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
self?.setIsEditing(true)
|
||||
})
|
||||
.eraseToAnyPublisher(),
|
||||
navItemTapped
|
||||
.filter { $0 == .cancel }
|
||||
.map { _ in .standard }
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
self?.setIsEditing(false)
|
||||
self?.editedDisplayName = self?.oldDisplayName
|
||||
})
|
||||
.eraseToAnyPublisher(),
|
||||
navItemTapped
|
||||
.filter { $0 == .done }
|
||||
.filter { [weak self] _ in self?.threadVariant == .contact }
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
self?.setIsEditing(false)
|
||||
|
||||
guard
|
||||
let threadId: String = self?.threadId,
|
||||
let editedDisplayName: String = self?.editedDisplayName
|
||||
else { return }
|
||||
|
||||
let updatedNickname: String = editedDisplayName
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self?.oldDisplayName = (updatedNickname.isEmpty ? nil : editedDisplayName)
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
try Profile
|
||||
.filter(id: threadId)
|
||||
.updateAll(
|
||||
db,
|
||||
Profile.Columns.nickname
|
||||
.set(to: (updatedNickname.isEmpty ? nil : editedDisplayName))
|
||||
)
|
||||
}
|
||||
})
|
||||
.map { _ in .standard }
|
||||
.eraseToAnyPublisher()
|
||||
)
|
||||
isEditing
|
||||
.map { isEditing in (isEditing ? .editing : .standard) }
|
||||
.removeDuplicates()
|
||||
.prepend(.standard) // Initial value
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -139,7 +99,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
id: .cancel,
|
||||
systemItem: .cancel,
|
||||
accessibilityIdentifier: "Cancel button"
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.setIsEditing(false)
|
||||
self?.editedDisplayName = self?.oldDisplayName
|
||||
}
|
||||
]
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -147,7 +110,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
|
||||
override var rightNavItems: AnyPublisher<[NavItem]?, Never> {
|
||||
navState
|
||||
.map { [weak self] navState -> [NavItem] in
|
||||
.map { [weak self, dependencies] navState -> [NavItem] in
|
||||
// Only show the 'Edit' button if it's a contact thread
|
||||
guard self?.threadVariant == .contact else { return [] }
|
||||
|
||||
|
@ -158,7 +121,29 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
id: .done,
|
||||
systemItem: .done,
|
||||
accessibilityIdentifier: "Done button"
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.setIsEditing(false)
|
||||
|
||||
guard
|
||||
self?.threadVariant == .contact,
|
||||
let threadId: String = self?.threadId,
|
||||
let editedDisplayName: String = self?.editedDisplayName
|
||||
else { return }
|
||||
|
||||
let updatedNickname: String = editedDisplayName
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self?.oldDisplayName = (updatedNickname.isEmpty ? nil : editedDisplayName)
|
||||
|
||||
dependencies.storage.writeAsync { db in
|
||||
try Profile
|
||||
.filter(id: threadId)
|
||||
.updateAll(
|
||||
db,
|
||||
Profile.Columns.nickname
|
||||
.set(to: (updatedNickname.isEmpty ? nil : editedDisplayName))
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
case .standard:
|
||||
|
@ -167,7 +152,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
id: .edit,
|
||||
systemItem: .edit,
|
||||
accessibilityIdentifier: "Edit button"
|
||||
)
|
||||
) { [weak self] in self?.setIsEditing(true) }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -196,8 +181,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
/// 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, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
.trackingConstantRegion { [weak self, dependencies, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
|
||||
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
|
||||
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
|
||||
.fetchOne(db)
|
||||
|
@ -387,7 +372,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
cancelStyle: .alert_text
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
Storage.shared.writeAsync { db in
|
||||
dependencies.storage.writeAsync { db in
|
||||
try MessageSender.leave(db, groupPublicKey: threadId)
|
||||
}
|
||||
}
|
||||
|
@ -435,7 +420,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
onTap: {
|
||||
let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true)
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
dependencies.storage.writeAsync { db in
|
||||
try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAll(
|
||||
|
@ -465,15 +450,19 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).mute",
|
||||
onTap: {
|
||||
let newValue: Bool = !(threadViewModel.threadMutedUntilTimestamp != nil)
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
dependencies.storage.writeAsync { db in
|
||||
let currentValue: TimeInterval? = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.select(.mutedUntilTimestamp)
|
||||
.asRequest(of: TimeInterval.self)
|
||||
.fetchOne(db)
|
||||
|
||||
try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAll(
|
||||
db,
|
||||
SessionThread.Columns.mutedUntilTimestamp.set(
|
||||
to: (newValue ?
|
||||
to: (currentValue == nil ?
|
||||
Date.distantFuture.timeIntervalSince1970 :
|
||||
nil
|
||||
)
|
||||
|
@ -538,7 +527,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
]
|
||||
}
|
||||
.removeDuplicates()
|
||||
.publisher(in: Storage.shared)
|
||||
.publisher(in: dependencies.storage, scheduling: dependencies.scheduler)
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
|
@ -575,7 +564,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
private func addUsersToOpenGoup(selectedUsers: Set<String>) {
|
||||
let threadId: String = self.threadId
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
dependencies.storage.writeAsync { db in
|
||||
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { return }
|
||||
|
||||
let urlString: String = "\(openGroup.server)/\(openGroup.roomToken)?public_key=\(openGroup.publicKey)"
|
||||
|
@ -622,7 +611,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
) {
|
||||
guard oldBlockedState != isBlocked else { return }
|
||||
|
||||
Storage.shared.writeAsync(
|
||||
dependencies.storage.writeAsync(
|
||||
updates: { db in
|
||||
try Contact
|
||||
.fetchOrCreate(db, id: threadId)
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -477,6 +477,7 @@
|
|||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
"CREATE_GROUP_BUTTON_TITLE" = "Create";
|
||||
|
|
|
@ -63,66 +63,8 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
// MARK: - Navigation
|
||||
|
||||
lazy var navState: AnyPublisher<NavState, Never> = {
|
||||
Publishers
|
||||
.MergeMany(
|
||||
isEditing
|
||||
.filter { $0 }
|
||||
.map { _ in .editing }
|
||||
.eraseToAnyPublisher(),
|
||||
navItemTapped
|
||||
.filter { $0 == .cancel }
|
||||
.map { _ in .standard }
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
self?.setIsEditing(false)
|
||||
self?.editedDisplayName = self?.oldDisplayName
|
||||
})
|
||||
.eraseToAnyPublisher(),
|
||||
navItemTapped
|
||||
.filter { $0 == .done }
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
let updatedNickname: String = (self?.editedDisplayName ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard !updatedNickname.isEmpty else {
|
||||
self?.transitionToScreen(
|
||||
ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "vc_settings_display_name_missing_error".localized(),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
),
|
||||
transitionType: .present
|
||||
)
|
||||
return
|
||||
}
|
||||
guard !ProfileManager.isToLong(profileName: updatedNickname) else {
|
||||
self?.transitionToScreen(
|
||||
ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "vc_settings_display_name_too_long_error".localized(),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
),
|
||||
transitionType: .present
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
self?.setIsEditing(false)
|
||||
self?.oldDisplayName = updatedNickname
|
||||
self?.updateProfile(
|
||||
name: updatedNickname,
|
||||
profilePicture: nil,
|
||||
profilePictureFilePath: nil,
|
||||
isUpdatingDisplayName: true,
|
||||
isUpdatingProfilePicture: false
|
||||
)
|
||||
})
|
||||
.map { _ in .standard }
|
||||
.eraseToAnyPublisher()
|
||||
)
|
||||
isEditing
|
||||
.map { isEditing in (isEditing ? .editing : .standard) }
|
||||
.removeDuplicates()
|
||||
.prepend(.standard) // Initial value
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -149,7 +91,10 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
id: .cancel,
|
||||
systemItem: .cancel,
|
||||
accessibilityIdentifier: "Cancel button"
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.setIsEditing(false)
|
||||
self?.editedDisplayName = self?.oldDisplayName
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +125,47 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
id: .done,
|
||||
systemItem: .done,
|
||||
accessibilityIdentifier: "Done button"
|
||||
)
|
||||
) { [weak self] in
|
||||
let updatedNickname: String = (self?.editedDisplayName ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard !updatedNickname.isEmpty else {
|
||||
self?.transitionToScreen(
|
||||
ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "vc_settings_display_name_missing_error".localized(),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
),
|
||||
transitionType: .present
|
||||
)
|
||||
return
|
||||
}
|
||||
guard !ProfileManager.isToLong(profileName: updatedNickname) else {
|
||||
self?.transitionToScreen(
|
||||
ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "vc_settings_display_name_too_long_error".localized(),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
),
|
||||
transitionType: .present
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
self?.setIsEditing(false)
|
||||
self?.oldDisplayName = updatedNickname
|
||||
self?.updateProfile(
|
||||
name: updatedNickname,
|
||||
profilePicture: nil,
|
||||
profilePictureFilePath: nil,
|
||||
isUpdatingDisplayName: true,
|
||||
isUpdatingProfilePicture: false
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1344,9 +1344,12 @@ public enum OpenGroupAPI {
|
|||
let method: String = (request.httpMethod ?? "GET")
|
||||
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
|
||||
let nonce: Data = Data(dependencies.nonceGenerator16.nonce())
|
||||
let serverPublicKeyData: Data = Data(hex: serverPublicKey)
|
||||
|
||||
guard let serverPublicKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
|
||||
guard let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes else { return nil }
|
||||
guard
|
||||
!serverPublicKeyData.isEmpty,
|
||||
let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes
|
||||
else { return nil }
|
||||
|
||||
/// Get a hash of any body content
|
||||
let bodyHash: Bytes? = {
|
||||
|
|
|
@ -1129,6 +1129,7 @@ extension OpenGroupManager {
|
|||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
sodium: SodiumType? = nil,
|
||||
box: BoxType? = nil,
|
||||
genericHash: GenericHashType? = nil,
|
||||
|
@ -1146,6 +1147,7 @@ extension OpenGroupManager {
|
|||
onionApi: onionApi,
|
||||
generalCache: generalCache,
|
||||
storage: storage,
|
||||
scheduler: scheduler,
|
||||
sodium: sodium,
|
||||
box: box,
|
||||
genericHash: genericHash,
|
||||
|
|
|
@ -510,10 +510,12 @@ public extension MessageViewModel {
|
|||
|
||||
// Interaction Info
|
||||
|
||||
let targetId: Int64 = (isTypingIndicator == true ?
|
||||
MessageViewModel.typingIndicatorId :
|
||||
MessageViewModel.genericId
|
||||
)
|
||||
let targetId: Int64 = {
|
||||
guard isTypingIndicator != true else { return MessageViewModel.typingIndicatorId }
|
||||
guard cellType != .dateHeader else { return -timestampMs }
|
||||
|
||||
return MessageViewModel.genericId
|
||||
}()
|
||||
self.rowId = targetId
|
||||
self.id = targetId
|
||||
self.variant = variant
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import Sodium
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
@ -66,6 +67,7 @@ public class SMKDependencies: Dependencies {
|
|||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
sodium: SodiumType? = nil,
|
||||
box: BoxType? = nil,
|
||||
genericHash: GenericHashType? = nil,
|
||||
|
@ -90,6 +92,7 @@ public class SMKDependencies: Dependencies {
|
|||
super.init(
|
||||
generalCache: generalCache,
|
||||
storage: storage,
|
||||
scheduler: scheduler,
|
||||
standardUserDefaults: standardUserDefaults,
|
||||
date: date
|
||||
)
|
||||
|
|
|
@ -25,8 +25,9 @@ extension Sodium {
|
|||
/// 64-byte blake2b hash then reduce to get the blinding factor
|
||||
public func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? {
|
||||
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
|
||||
guard let serverPubKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
|
||||
guard let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
|
||||
let serverPubKeyData: Data = Data(hex: serverPublicKey)
|
||||
|
||||
guard !serverPubKeyData.isEmpty, let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
|
@ -11,6 +12,7 @@ extension SMKDependencies {
|
|||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
sodium: SodiumType? = nil,
|
||||
box: BoxType? = nil,
|
||||
genericHash: GenericHashType? = nil,
|
||||
|
@ -26,6 +28,7 @@ extension SMKDependencies {
|
|||
onionApi: (onionApi ?? self._onionApi.wrappedValue),
|
||||
generalCache: (generalCache ?? self._generalCache.wrappedValue),
|
||||
storage: (storage ?? self._storage.wrappedValue),
|
||||
scheduler: (scheduler ?? self._scheduler.wrappedValue),
|
||||
sodium: (sodium ?? self._sodium.wrappedValue),
|
||||
box: (box ?? self._box.wrappedValue),
|
||||
genericHash: (genericHash ?? self._genericHash.wrappedValue),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
|
@ -12,6 +13,7 @@ extension OpenGroupManager.OGMDependencies {
|
|||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
sodium: SodiumType? = nil,
|
||||
box: BoxType? = nil,
|
||||
genericHash: GenericHashType? = nil,
|
||||
|
@ -28,6 +30,7 @@ extension OpenGroupManager.OGMDependencies {
|
|||
onionApi: (onionApi ?? self._onionApi.wrappedValue),
|
||||
generalCache: (generalCache ?? self._generalCache.wrappedValue),
|
||||
storage: (storage ?? self._storage.wrappedValue),
|
||||
scheduler: (scheduler ?? self._scheduler.wrappedValue),
|
||||
sodium: (sodium ?? self._sodium.wrappedValue),
|
||||
box: (box ?? self._box.wrappedValue),
|
||||
genericHash: (genericHash ?? self._genericHash.wrappedValue),
|
||||
|
|
|
@ -14,8 +14,8 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
|
||||
override func spec() {
|
||||
var mockStorage: Storage!
|
||||
var dataChangeCancellable: AnyCancellable?
|
||||
var otherCancellables: [AnyCancellable] = []
|
||||
var cancellables: [AnyCancellable] = []
|
||||
var dependencies: Dependencies!
|
||||
var viewModel: ThreadDisappearingMessagesViewModel!
|
||||
|
||||
describe("a ThreadDisappearingMessagesViewModel") {
|
||||
|
@ -31,6 +31,10 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
SNUIKit.migrations()
|
||||
]
|
||||
)
|
||||
dependencies = Dependencies(
|
||||
storage: mockStorage,
|
||||
scheduler: .immediate
|
||||
)
|
||||
mockStorage.write { db in
|
||||
try SessionThread(
|
||||
id: "TestId",
|
||||
|
@ -38,26 +42,26 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
).insert(db)
|
||||
}
|
||||
viewModel = ThreadDisappearingMessagesViewModel(
|
||||
storage: mockStorage,
|
||||
scheduling: .immediate,
|
||||
dependencies: dependencies,
|
||||
threadId: "TestId",
|
||||
config: DisappearingMessagesConfiguration.defaultWith("TestId")
|
||||
)
|
||||
dataChangeCancellable = viewModel.observableSettingsData
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { viewModel.updateSettings($0) }
|
||||
)
|
||||
cancellables.append(
|
||||
viewModel.observableSettingsData
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { viewModel.updateSettings($0) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
afterEach {
|
||||
dataChangeCancellable?.cancel()
|
||||
otherCancellables.forEach { $0.cancel() }
|
||||
cancellables.forEach { $0.cancel() }
|
||||
|
||||
mockStorage = nil
|
||||
dataChangeCancellable = nil
|
||||
otherCancellables = []
|
||||
cancellables = []
|
||||
dependencies = nil
|
||||
viewModel = nil
|
||||
}
|
||||
|
||||
|
@ -118,17 +122,18 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
_ = try config.saved(db)
|
||||
}
|
||||
viewModel = ThreadDisappearingMessagesViewModel(
|
||||
storage: mockStorage,
|
||||
scheduling: .immediate,
|
||||
dependencies: dependencies,
|
||||
threadId: "TestId",
|
||||
config: config
|
||||
)
|
||||
dataChangeCancellable = viewModel.observableSettingsData
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { viewModel.updateSettings($0) }
|
||||
)
|
||||
cancellables.append(
|
||||
viewModel.observableSettingsData
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { viewModel.updateSettings($0) }
|
||||
)
|
||||
)
|
||||
|
||||
expect(viewModel.settingsData.first?.elements.first)
|
||||
.to(
|
||||
|
@ -165,7 +170,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
it("has no right bar button") {
|
||||
var items: [ParentType.NavItem]?
|
||||
|
||||
otherCancellables.append(
|
||||
cancellables.append(
|
||||
viewModel.rightNavItems
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
|
@ -181,7 +186,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
var items: [ParentType.NavItem]?
|
||||
|
||||
beforeEach {
|
||||
otherCancellables.append(
|
||||
cancellables.append(
|
||||
viewModel.rightNavItems
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
|
@ -208,7 +213,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
it("dismisses the screen") {
|
||||
var didDismissScreen: Bool = false
|
||||
|
||||
otherCancellables.append(
|
||||
cancellables.append(
|
||||
viewModel.dismissScreen
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,7 @@ import GRDB
|
|||
import PromiseKit
|
||||
import SignalCoreKit
|
||||
|
||||
public final class Storage {
|
||||
open class Storage {
|
||||
private static let dbFileName: String = "Session.sqlite"
|
||||
private static let keychainService: String = "TSKeyChainService"
|
||||
private static let dbCipherKeySpecKey: String = "GRDBDatabaseCipherKeySpec"
|
||||
|
@ -305,17 +305,17 @@ public final class Storage {
|
|||
|
||||
// MARK: - Functions
|
||||
|
||||
@discardableResult public func write<T>(updates: (Database) throws -> T?) -> T? {
|
||||
@discardableResult public final func write<T>(updates: (Database) throws -> T?) -> T? {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
|
||||
|
||||
return try? dbWriter.write(updates)
|
||||
}
|
||||
|
||||
public func writeAsync<T>(updates: @escaping (Database) throws -> T) {
|
||||
open func writeAsync<T>(updates: @escaping (Database) throws -> T) {
|
||||
writeAsync(updates: updates, completion: { _, _ in })
|
||||
}
|
||||
|
||||
public func writeAsync<T>(updates: @escaping (Database) throws -> T, completion: @escaping (Database, Swift.Result<T, Error>) throws -> Void) {
|
||||
open func writeAsync<T>(updates: @escaping (Database) throws -> T, completion: @escaping (Database, Swift.Result<T, Error>) throws -> Void) {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return }
|
||||
|
||||
dbWriter.asyncWrite(
|
||||
|
@ -326,7 +326,7 @@ public final class Storage {
|
|||
)
|
||||
}
|
||||
|
||||
@discardableResult public func read<T>(_ value: (Database) throws -> T?) -> T? {
|
||||
@discardableResult public final func read<T>(_ value: (Database) throws -> T?) -> T? {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
|
||||
|
||||
return try? dbWriter.read(value)
|
||||
|
@ -401,6 +401,7 @@ public extension Storage {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: Can't overrwrite this in `SynchronousStorage` since it's in an extension
|
||||
@discardableResult func writeAsync<T>(updates: @escaping (Database) throws -> Promise<T>) -> Promise<T> {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
|
||||
return Promise(error: StorageError.databaseInvalid)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
open class Dependencies {
|
||||
public var _generalCache: Atomic<Atomic<GeneralCacheType>?>
|
||||
|
@ -15,6 +16,12 @@ open class Dependencies {
|
|||
set { _storage.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
public var _scheduler: Atomic<ValueObservationScheduler?>
|
||||
public var scheduler: ValueObservationScheduler {
|
||||
get { Dependencies.getValueSettingIfNull(&_scheduler) { Storage.defaultPublisherScheduler } }
|
||||
set { _scheduler.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
public var _standardUserDefaults: Atomic<UserDefaultsType?>
|
||||
public var standardUserDefaults: UserDefaultsType {
|
||||
get { Dependencies.getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } }
|
||||
|
@ -32,11 +39,13 @@ open class Dependencies {
|
|||
public init(
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
standardUserDefaults: UserDefaultsType? = nil,
|
||||
date: Date? = nil
|
||||
) {
|
||||
_generalCache = Atomic(generalCache)
|
||||
_storage = Atomic(storage)
|
||||
_scheduler = Atomic(scheduler)
|
||||
_standardUserDefaults = Atomic(standardUserDefaults)
|
||||
_date = Atomic(date)
|
||||
}
|
||||
|
@ -52,12 +61,4 @@ open class Dependencies {
|
|||
|
||||
return value
|
||||
}
|
||||
|
||||
// 0 libswiftCore.dylib 0x00000001999fd40c _swift_release_dealloc + 32 (HeapObject.cpp:703)
|
||||
// 1 SessionMessagingKit 0x0000000106aa958c 0x106860000 + 2397580
|
||||
// 2 libswiftCore.dylib 0x00000001999fd424 _swift_release_dealloc + 56 (HeapObject.cpp:703)
|
||||
// 3 SessionUtilitiesKit 0x0000000106cbd980 static Dependencies.getValueSettingIfNull<A>(_:_:) + 264 (Dependencies.swift:49)
|
||||
// 4 SessionMessagingKit 0x0000000106aa90f4 closure #1 in SMKDependencies.sign.getter + 112 (SMKDependencies.swift:17)
|
||||
// 5 SessionUtilitiesKit 0x0000000106cbd974 static Dependencies.getValueSettingIfNull<A>(_:_:) + 252 (Dependencies.swift:48)
|
||||
// 6 SessionMessagingKit 0x000000010697aef8 specialized static OpenGroupAPI.sign(_:messageBytes:for:fallbackSigningType:using:) + 1158904 (OpenGroupAPI.swift:1190)
|
||||
}
|
||||
|
|
|
@ -44,20 +44,6 @@ public extension String {
|
|||
return localizedString
|
||||
}
|
||||
|
||||
func dataFromHex() -> Data? {
|
||||
guard self.count > 0 && (self.count % 2) == 0 else { return nil }
|
||||
|
||||
let chars = self.map { $0 }
|
||||
let bytes: [UInt8] = stride(from: 0, to: chars.count, by: 2)
|
||||
.map { index -> String in String(chars[index]) + String(chars[index + 1]) }
|
||||
.compactMap { (str: String) -> UInt8? in UInt8(str, radix: 16) }
|
||||
|
||||
guard bytes.count > 0 else { return nil }
|
||||
guard (self.count / bytes.count) == 2 else { return nil }
|
||||
|
||||
return Data(bytes)
|
||||
}
|
||||
|
||||
func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
|
||||
var ranges: [Range<Index>] = []
|
||||
|
||||
|
|
|
@ -22,7 +22,11 @@ public class ToastController: ToastViewDelegate {
|
|||
|
||||
// MARK: Public
|
||||
|
||||
public func presentToastView(fromBottomOfView view: UIView, inset: CGFloat) {
|
||||
public func presentToastView(
|
||||
fromBottomOfView view: UIView,
|
||||
inset: CGFloat,
|
||||
duration: DispatchTimeInterval = .milliseconds(1500)
|
||||
) {
|
||||
Logger.debug("")
|
||||
toastView.alpha = 0
|
||||
view.addSubview(toastView)
|
||||
|
@ -46,7 +50,7 @@ public class ToastController: ToastViewDelegate {
|
|||
self.toastView.alpha = 1
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
|
||||
// intentional strong reference to self.
|
||||
// As with an AlertController, the caller likely expects toast to
|
||||
// be presented and dismissed without maintaining a strong reference to ToastController
|
||||
|
|
19
_SharedTestUtilities/CombineExtensions.swift
Normal file
19
_SharedTestUtilities/CombineExtensions.swift
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public extension AnyPublisher {
|
||||
func firstValue() -> Output? {
|
||||
var value: Output?
|
||||
|
||||
_ = self
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { result in value = result }
|
||||
)
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
|
@ -6,16 +6,16 @@ import Curve25519Kit
|
|||
|
||||
extension Box.KeyPair: Mocked {
|
||||
static var mockValue: Box.KeyPair = Box.KeyPair(
|
||||
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
|
||||
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
|
||||
publicKey: Data(hex: TestConstants.publicKey).bytes,
|
||||
secretKey: Data(hex: TestConstants.edSecretKey).bytes
|
||||
)
|
||||
}
|
||||
|
||||
extension ECKeyPair: Mocked {
|
||||
static var mockValue: Self {
|
||||
try! Self.init(
|
||||
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
|
||||
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
|
||||
publicKeyData: Data(hex: TestConstants.publicKey),
|
||||
privateKeyData: Data(hex: TestConstants.privateKey)
|
||||
)
|
||||
}
|
||||
}
|
28
_SharedTestUtilities/SynchronousStorage.swift
Normal file
28
_SharedTestUtilities/SynchronousStorage.swift
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class SynchronousStorage: Storage {
|
||||
override func writeAsync<T>(updates: @escaping (Database) throws -> T) {
|
||||
super.write(updates: updates)
|
||||
}
|
||||
|
||||
override func writeAsync<T>(updates: @escaping (Database) throws -> T, completion: @escaping (Database, Swift.Result<T, Error>) throws -> Void) {
|
||||
super.write { db in
|
||||
do {
|
||||
var result: T?
|
||||
try db.inTransaction {
|
||||
result = try updates(db)
|
||||
return .commit
|
||||
}
|
||||
try? completion(db, .success(result!))
|
||||
}
|
||||
catch {
|
||||
try? completion(db, .failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue