Added toast and info message deletion, fixed layout issues & unit tests

Added a toast when hitting the emoji reacts rate limit
Added the ability to delete info messages
Fixed some odd layout behaviours with the VisibleMessageCell
Fixed some layout issues with reactions
Removed some unneeded custom code
This commit is contained in:
Morgan Pretty 2022-10-05 18:44:25 +11:00
parent db54bf657e
commit 27e0981913
60 changed files with 1280 additions and 1262 deletions

View File

@ -573,6 +573,13 @@
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; };
FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacy.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 */; }; 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 */; }; FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7552553A3AB00C340D1 /* VisibleMessage+Quote.swift */; };
FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5FB2554B0A000555489 /* MessageReceiver.swift */; }; FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5FB2554B0A000555489 /* MessageReceiver.swift */; };
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF224255B6D5D007E1867 /* SignalAttachment.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 */; }; FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; }; FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.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 */; }; FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; };
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Colors.swift"; sourceTree = "<group>"; };
@ -3374,7 +3388,7 @@
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */, C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */, C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */,
C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */,
FD83B9BC27CF2215005E1583 /* SharedTest */, FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */,
FD71160A28D00BAE00B47552 /* SessionTests */, FD71160A28D00BAE00B47552 /* SessionTests */,
FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */,
FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */, FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */,
@ -3897,15 +3911,18 @@
path = General; path = General;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FD83B9BC27CF2215005E1583 /* SharedTest */ = { FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FDC290A527D860CE005DAE71 /* Mock.swift */, FDC290A527D860CE005DAE71 /* Mock.swift */,
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */,
FD83B9BD27CF2243005E1583 /* TestConstants.swift */, FD83B9BD27CF2243005E1583 /* TestConstants.swift */,
FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */, FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */,
FD078E4727E02561000769AF /* CommonMockedExtensions.swift */, FD078E4727E02561000769AF /* CommonMockedExtensions.swift */,
FD23EA6028ED0B260058676E /* CombineExtensions.swift */,
FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */,
); );
path = SharedTest; path = _SharedTestUtilities;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FD83B9C127CF33EE005E1583 /* Models */ = { FD83B9C127CF33EE005E1583 /* Models */ = {
@ -4052,7 +4069,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FDC438BC27BB2AB400C60D73 /* Mockable.swift */, FDC438BC27BB2AB400C60D73 /* Mockable.swift */,
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */,
FD859EF327C2F49200510D0C /* MockSodium.swift */, FD859EF327C2F49200510D0C /* MockSodium.swift */,
FD3C906E27E43E8700CD579F /* MockBox.swift */, FD3C906E27E43E8700CD579F /* MockBox.swift */,
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */, FD859EF927C2F5C500510D0C /* MockGenericHash.swift */,
@ -5752,8 +5768,15 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */, FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */,
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.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 */, FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */,
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */,
FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */,
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -5761,10 +5784,13 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */, FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */, FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */, FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
); );
@ -5789,6 +5815,7 @@
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */, FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */, FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */,
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */, FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */, FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */,
@ -5800,6 +5827,7 @@
FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */,
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */, FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */,
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */,
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */,
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,

View File

@ -118,6 +118,18 @@
</BuildableReference> </BuildableReference>
</CodeCoverageTargets> </CodeCoverageTargets>
<Testables> <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 <TestableReference
skipped = "NO" skipped = "NO"
parallelizable = "YES" parallelizable = "YES"
@ -142,18 +154,6 @@
ReferencedContainer = "container:Session.xcodeproj"> ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference> </BuildableReference>
</TestableReference> </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> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction

View File

@ -45,27 +45,18 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
return result return result
}() }()
private lazy var fadeView: UIView = { private lazy var fadeView: GradientView = {
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64) let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top)
.map { $0 + Values.veryLargeSpacing })
let result = UIView() .defaulting(to: 64)
var frame = UIScreen.main.bounds
frame.size.height = height let result: GradientView = GradientView()
result.themeBackgroundGradient = [
let layer = CAGradientLayer() .value(.backgroundPrimary, alpha: 0.4),
layer.frame = frame .value(.backgroundPrimary, alpha: 0)
result.layer.insertSublayer(layer, at: 0) ]
result.set(.height, to: height) 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 return result
}() }()

View File

@ -26,27 +26,18 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
return result return result
}() }()
private lazy var fadeView: UIView = { private lazy var fadeView: GradientView = {
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64) let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top)
.map { $0 + Values.veryLargeSpacing })
let result = UIView() .defaulting(to: 64)
var frame = UIScreen.main.bounds
frame.size.height = height let result: GradientView = GradientView()
result.themeBackgroundGradient = [
let layer = CAGradientLayer() .value(.backgroundPrimary, alpha: 0.4),
layer.frame = frame .value(.backgroundPrimary, alpha: 0)
result.layer.insertSublayer(layer, at: 0) ]
result.set(.height, to: height) 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 return result
}() }()

View File

@ -108,12 +108,15 @@ extension ContextMenuVC {
currentThreadIsMessageRequest: Bool, currentThreadIsMessageRequest: Bool,
delegate: ContextMenuActionDelegate? delegate: ContextMenuActionDelegate?
) -> [Action]? { ) -> [Action]? {
// No context items for info messages switch cellViewModel.variant {
guard cellViewModel.variant != .standardIncomingDeleted else { case .standardIncomingDeleted, .infoCall,
return [ Action.delete(cellViewModel, delegate) ] .infoScreenshotNotification, .infoMediaSavedNotification,
} .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
guard cellViewModel.variant == .standardOutgoing || cellViewModel.variant == .standardIncoming else { .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
return nil // Let the user delete info messages and unsent messages
return [ Action.delete(cellViewModel, delegate) ]
case .standardOutgoing, .standardIncoming: break
} }
let canReply: Bool = ( let canReply: Bool = (

View File

@ -225,7 +225,8 @@ final class ContextMenuVC: UIViewController {
menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX) menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX)
emojiBar.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 // Tap gesture

View File

@ -766,8 +766,9 @@ extension ConversationVC:
let index = self.viewModel.interactionData[sectionIndex] let index = self.viewModel.interactionData[sectionIndex]
.elements .elements
.firstIndex(of: cellViewModel), .firstIndex(of: cellViewModel),
let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? VisibleMessageCell, let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? MessageCell,
let snapshot = cell.snContentView.snapshotView(afterScreenUpdates: false), let contextSnapshotView: UIView = cell.contextSnapshotView,
let snapshot = contextSnapshotView.snapshotView(afterScreenUpdates: false),
contextMenuWindow == nil, contextMenuWindow == nil,
let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( let actions: [ContextMenuVC.Action] = ContextMenuVC.actions(
for: cellViewModel, for: cellViewModel,
@ -789,7 +790,7 @@ extension ConversationVC:
self.contextMenuWindow = ContextMenuWindow() self.contextMenuWindow = ContextMenuWindow()
self.contextMenuVC = ContextMenuVC( self.contextMenuVC = ContextMenuVC(
snapshot: snapshot, snapshot: snapshot,
frame: cell.convert(cell.snContentView.frame, to: keyWindow), frame: contextSnapshotView.convert(contextSnapshotView.bounds, to: keyWindow),
cellViewModel: cellViewModel, cellViewModel: cellViewModel,
actions: actions actions: actions
) { [weak self] in ) { [weak self] in
@ -1218,7 +1219,18 @@ extension ConversationVC:
guard guard
recentReactionTimestamps.count < 20 || recentReactionTimestamps.count < 20 ||
(sentTimestamp - (recentReactionTimestamps.first ?? sentTimestamp)) > (60 * 1000) (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 { General.cache.mutate {
$0.recentReactionTimestamps = Array($0.recentReactionTimestamps $0.recentReactionTimestamps = Array($0.recentReactionTimestamps
@ -1593,17 +1605,22 @@ extension ConversationVC:
} }
func delete(_ cellViewModel: MessageViewModel) { func delete(_ cellViewModel: MessageViewModel) {
// Only allow deletion on incoming and outgoing messages switch cellViewModel.variant {
guard cellViewModel.variant != .standardIncomingDeleted else { case .standardIncomingDeleted, .infoCall,
Storage.shared.writeAsync { db in .infoScreenshotNotification, .infoMediaSavedNotification,
_ = try Interaction .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
.filter(id: cellViewModel.id) .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
.deleteAll(db) // Info messages and unsent messages should just trigger a local
} // deletion (they are created as side effects so we wouldn't be
return // able to delete them for all participants anyway)
} Storage.shared.writeAsync { db in
guard cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardOutgoing else { _ = try Interaction
return .filter(id: cellViewModel.id)
.deleteAll(db)
}
return
case .standardOutgoing, .standardIncoming: break
} }
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId

View File

@ -705,10 +705,23 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
let newSectionIndex: Int = updatedData.firstIndex(where: { $0.model == .messages }), let newSectionIndex: Int = updatedData.firstIndex(where: { $0.model == .messages }),
let newFirstItemIndex: Int = updatedData[newSectionIndex].elements let newFirstItemIndex: Int = updatedData[newSectionIndex].elements
.firstIndex(where: { item -> Bool in .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? 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() .sorted()
.first, .first,
let newVisibleIndex: Int = updatedData[newSectionIndex].elements let newVisibleIndex: Int = updatedData[newSectionIndex].elements
@ -722,7 +735,9 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
return ItemChangeInfo( return ItemChangeInfo(
isInsertAtTop: ( isInsertAtTop: (
newSectionIndex > oldSectionIndex || 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), firstIndexIsVisible: (firstVisibleIndexPath.row == 0),
visibleIndexPath: IndexPath(row: newVisibleIndex, section: newSectionIndex), visibleIndexPath: IndexPath(row: newVisibleIndex, section: newSectionIndex),

View File

@ -9,14 +9,18 @@ final class CallMessageCell: MessageCell {
private static let inset = Values.mediumSpacing private static let inset = Values.mediumSpacing
private static let margin = UIScreen.main.bounds.width * 0.1 private static let margin = UIScreen.main.bounds.width * 0.1
private lazy var iconImageViewWidthConstraint = iconImageView.set(.width, to: 0) private var isHandlingLongPress: Bool = false
private lazy var iconImageViewHeightConstraint = iconImageView.set(.height, to: 0)
private lazy var infoImageViewWidthConstraint = infoImageView.set(.width, to: 0) override var contextSnapshotView: UIView? { return container }
private lazy var infoImageViewHeightConstraint = infoImageView.set(.height, to: 0)
// MARK: - UI // 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 iconImageView: UIImageView = UIImageView()
private lazy var infoImageView: UIImageView = { private lazy var infoImageView: UIImageView = {
let result: UIImageView = UIImageView( let result: UIImageView = UIImageView(
@ -28,15 +32,6 @@ final class CallMessageCell: MessageCell {
return result 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 = { private lazy var label: UILabel = {
let result: UILabel = UILabel() let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: Values.smallFontSize) result.font = .boldSystemFont(ofSize: Values.smallFontSize)
@ -80,15 +75,6 @@ final class CallMessageCell: MessageCell {
return result 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 // MARK: - Lifecycle
override func setUpViewHierarchy() { override func setUpViewHierarchy() {
@ -96,16 +82,18 @@ final class CallMessageCell: MessageCell {
iconImageViewWidthConstraint.isActive = true iconImageViewWidthConstraint.isActive = true
iconImageViewHeightConstraint.isActive = true iconImageViewHeightConstraint.isActive = true
addSubview(stackView) addSubview(container)
container.autoPinWidthToSuperview() topConstraint.isActive = true
stackView.pin(.left, to: .left, of: self, withInset: CallMessageCell.margin) container.pin(.left, to: .left, of: self, withInset: CallMessageCell.margin)
stackView.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset) container.pin(.right, to: .right, of: self, withInset: -CallMessageCell.margin)
stackView.pin(.right, to: .right, of: self, withInset: -CallMessageCell.margin) container.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset)
stackView.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset)
} }
override func setUpGestureRecognizers() { override func setUpGestureRecognizers() {
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
addGestureRecognizer(longPressRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapGestureRecognizer.numberOfTapsRequired = 1 tapGestureRecognizer.numberOfTapsRequired = 1
addGestureRecognizer(tapGestureRecognizer) addGestureRecognizer(tapGestureRecognizer)
@ -130,6 +118,7 @@ final class CallMessageCell: MessageCell {
else { return } else { return }
self.viewModel = cellViewModel self.viewModel = cellViewModel
self.topConstraint.constant = (cellViewModel.shouldShowDateHeader ? 0 : CallMessageCell.inset)
iconImageView.image = { iconImageView.image = {
switch messageInfo.state { switch messageInfo.state {
@ -157,12 +146,24 @@ final class CallMessageCell: MessageCell {
infoImageViewHeightConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) infoImageViewHeightConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0)
label.text = cellViewModel.body label.text = cellViewModel.body
timestampLabel.text = cellViewModel.dateForUI.formattedForDisplay
} }
override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) { 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) { @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard guard
let cellViewModel: MessageViewModel = self.viewModel, let cellViewModel: MessageViewModel = self.viewModel,

View File

@ -76,15 +76,11 @@ final class DocumentView: UIView {
stackView.axis = .horizontal stackView.axis = .horizontal
stackView.spacing = Values.mediumSpacing stackView.spacing = Values.mediumSpacing
stackView.alignment = .center stackView.alignment = .center
stackView.layoutMargins = UIEdgeInsets(
top: Values.smallSpacing,
leading: Values.mediumSpacing,
bottom: Values.smallSpacing,
trailing: Values.mediumSpacing
)
stackView.isLayoutMarginsRelativeArrangement = true
addSubview(stackView) 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(.top, to: .top, of: self)
imageBackgroundView.pin(.leading, to: .leading, of: self) imageBackgroundView.pin(.leading, to: .leading, of: self)

View File

@ -2,11 +2,17 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
final class ReactionContainerView: UIView { final class ReactionContainerView: UIView {
var showingAllReactions = false private static let arrowSize: CGSize = CGSize(width: 15, height: 13)
private var showNumbers = true 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 = isIPhone6OrSmaller ? 5 : 6 private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6
private var oldSize: CGSize = .zero private var oldSize: CGSize = .zero
@ -15,6 +21,16 @@ final class ReactionContainerView: UIView {
// MARK: - UI // 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 = { private lazy var mainStackView: UIStackView = {
let result: UIStackView = UIStackView(arrangedSubviews: [ reactionContainerView, collapseButton ]) let result: UIStackView = UIStackView(arrangedSubviews: [ reactionContainerView, collapseButton ])
result.axis = .vertical result.axis = .vertical
@ -24,6 +40,8 @@ final class ReactionContainerView: UIView {
return result return result
}() }()
var expandButton: ExpandingReactionButton?
private lazy var reactionContainerView: UIStackView = { private lazy var reactionContainerView: UIStackView = {
let result: UIStackView = UIStackView() let result: UIStackView = UIStackView()
result.axis = .vertical result.axis = .vertical
@ -33,34 +51,36 @@ final class ReactionContainerView: UIView {
return result return result
}() }()
var expandButton: ExpandingReactionButton? lazy var collapseButton: UIView = {
let arrow: UIImageView = UIImageView(
var collapseButton: UIStackView = {
let arrow = UIImageView(
image: UIImage(named: "ic_chevron_up")? image: UIImage(named: "ic_chevron_up")?
.resizedImage(to: CGSize(width: 15, height: 13))? .resizedImage(to: ReactionContainerView.arrowSize)?
.withRenderingMode(.alwaysTemplate) .withRenderingMode(.alwaysTemplate)
) )
arrow.themeTintColor = .textPrimary arrow.themeTintColor = .textPrimary
let textLabel: UILabel = UILabel() 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.font = .systemFont(ofSize: Values.verySmallFontSize)
textLabel.text = "Show less" textLabel.text = "Show less"
textLabel.themeTextColor = .textPrimary textLabel.themeTextColor = .textPrimary
let leftSpacer: UIView = UIView.hStretchingSpacer() let result: UIView = UIView()
let rightSpacer: UIView = UIView.hStretchingSpacer()
let result: UIStackView = UIStackView(arrangedSubviews: [
leftSpacer,
arrow,
textLabel,
rightSpacer
])
result.isLayoutMarginsRelativeArrangement = true
result.spacing = Values.verySmallSpacing
result.alignment = .center
result.isHidden = true 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 return result
}() }()
@ -84,123 +104,181 @@ final class ReactionContainerView: UIView {
addSubview(mainStackView) addSubview(mainStackView)
mainStackView.pin(to: self) mainStackView.pin(to: self)
collapseButton.set(.width, to: .width, of: mainStackView) reactionContainerView.set(.width, to: .width, of: mainStackView)
} }
override func layoutSubviews() { override func layoutSubviews() {
super.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) // 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 } guard frame != CGRect.zero, frame.size != oldSize else { return }
collapseButton.layoutMargins = UIEdgeInsets( var targetSuperview: UIView? = {
top: 0, var result: UIView? = self.superview
leading: -frame.minX,
bottom: 0, while result != nil, result?.isKind(of: UITableViewCell.self) != true {
trailing: -((superview?.frame.width ?? 0) - frame.maxX) 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 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.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.showNumbers = showNumbers
self.reactionViews = []
self.reactionContainerView.arrangedSubviews.forEach { $0.removeFromSuperview() }
prepareForUpdate() // Generate the lines of reactions (if the 'collapsedCount' matches the total number of
// reactions then just show them app)
if showingAllReactions { if showingAllReactions || self.collapsedCount >= reactions.count {
updateAllReactions() self.updateAllReactions(reactions, maxWidth: maxWidth, showNumbers: showNumbers)
} }
else { 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]) { private func createLineStackView() -> UIStackView {
let stackView = UIStackView() let result: UIStackView = UIStackView()
stackView.axis = .horizontal result.axis = .horizontal
stackView.spacing = Values.smallSpacing result.spacing = Values.smallSpacing
stackView.alignment = .center result.alignment = .center
result.set(.height, to: ReactionButton.height)
var displayedReactions: [ReactionViewModel] return result
var expandButtonReactions: [EmojiWithSkinTones] }
private func updateCollapsedReactions(
_ reactions: [ReactionViewModel],
maxWidth: CGFloat,
showNumbers: Bool
) {
guard !reactions.isEmpty else { return }
if reactions.count > maxEmojisPerLine { let maxSize: CGSize = CGSize(width: maxWidth, height: 9999)
displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)]) let stackView: UIStackView = createLineStackView()
expandButtonReactions = Array(reactions[(maxEmojisPerLine - 2)...maxEmojisPerLine]) let displayedReactions: [ReactionViewModel] = Array(reactions.prefix(upTo: self.collapsedCount))
.map { $0.emoji } let expandButtonReactions: [EmojiWithSkinTones] = reactions
} .suffix(from: self.collapsedCount)
else { .prefix(3)
displayedReactions = reactions .map { $0.emoji }
expandButtonReactions = []
}
for reaction in displayedReactions { for reaction in displayedReactions {
let reactionView = ReactionButton(viewModel: reaction, showNumber: showNumbers) let reactionView = ReactionButton(viewModel: reaction, showNumber: showNumbers)
let reactionViewWidth: CGFloat = reactionView.systemLayoutSizeFitting(maxSize).width
stackView.addArrangedSubview(reactionView) stackView.addArrangedSubview(reactionView)
reactionViews.append(reactionView) reactionViews.append(reactionView)
reactionView.set(.width, to: reactionViewWidth)
} }
if expandButtonReactions.count > 0 { self.expandButton = {
let expandButton: ExpandingReactionButton = ExpandingReactionButton(emojis: expandButtonReactions) guard !expandButtonReactions.isEmpty else { return nil }
stackView.addArrangedSubview(expandButton)
let result: ExpandingReactionButton = ExpandingReactionButton(emojis: expandButtonReactions)
stackView.addArrangedSubview(result)
self.expandButton = expandButton return result
} }()
else {
expandButton = nil
}
reactionContainerView.addArrangedSubview(stackView) reactionContainerView.addArrangedSubview(stackView)
} }
private func updateAllReactions() { private func updateAllReactions(
var reactions = self.reactions _ reactions: [ReactionViewModel],
var numberOfLines = 0 maxWidth: CGFloat,
showNumbers: Bool
) {
guard !reactions.isEmpty else { return }
while reactions.count > 0 { let maxSize: CGSize = CGSize(width: maxWidth, height: 9999)
var line: [ReactionViewModel] = [] 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 { // Check if we need to create a new line
line.append(reactions.removeFirst()) let stackViewWidth: CGFloat = (lineStackView.arrangedSubviews.isEmpty ?
0 :
lineStackView.systemLayoutSizeFitting(maxSize).width
)
if stackViewWidth + reactionViewWidth > maxWidth {
lineStackView = createLineStackView()
reactionContainerView.addArrangedSubview(lineStackView)
} }
updateCollapsedReactions(line) lineStackView.addArrangedSubview(reactionView)
numberOfLines += 1 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() { public func showAllEmojis() {
guard !showingAllReactions else { return } guard !showingAllReactions else { return }
showingAllReactions = true update(reactions, maxWidth: maxWidth, showingAllReactions: true, showNumbers: showNumbers)
update(reactions, showNumbers: showNumbers)
} }
public func showLessEmojis() { public func showLessEmojis() {
guard showingAllReactions else { return } guard showingAllReactions else { return }
showingAllReactions = false update(reactions, maxWidth: maxWidth, showingAllReactions: false, showNumbers: showNumbers)
update(reactions, showNumbers: showNumbers)
} }
} }

View File

@ -15,10 +15,29 @@ final class ReactionButton: UIView {
// MARK: - Settings // MARK: - Settings
private var height: CGFloat = 22 public static var height: CGFloat = 22
private var fontSize: CGFloat = Values.verySmallFontSize private var fontSize: CGFloat = Values.verySmallFontSize
private var spacing: CGFloat = Values.verySmallSpacing 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 // MARK: - Lifecycle
init(viewModel: ReactionViewModel, showNumber: Bool = true) { init(viewModel: ReactionViewModel, showNumber: Bool = true) {
@ -28,6 +47,7 @@ final class ReactionButton: UIView {
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
setUpViewHierarchy() setUpViewHierarchy()
update(with: viewModel, showNumber: showNumber)
} }
override init(frame: CGRect) { override init(frame: CGRect) {
@ -39,35 +59,45 @@ final class ReactionButton: UIView {
} }
private func setUpViewHierarchy() { private func setUpViewHierarchy() {
let emojiLabel: UILabel = UILabel()
emojiLabel.font = .systemFont(ofSize: fontSize)
emojiLabel.text = viewModel.emoji.rawValue emojiLabel.text = viewModel.emoji.rawValue
let stackView: UIStackView = UIStackView(arrangedSubviews: [ emojiLabel ]) let stackView: UIStackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ])
stackView.axis = .horizontal stackView.axis = .horizontal
stackView.spacing = spacing stackView.spacing = spacing
stackView.alignment = .center stackView.alignment = .center
stackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.smallSpacing, bottom: 0, right: Values.smallSpacing)
stackView.isLayoutMarginsRelativeArrangement = true
addSubview(stackView) 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) themeBorderColor = (viewModel.showBorder ? .primary : .clear)
themeBackgroundColor = .messageBubble_incomingBackground themeBackgroundColor = .messageBubble_incomingBackground
layer.cornerRadius = (self.height / 2) layer.cornerRadius = (ReactionButton.height / 2)
layer.borderWidth = 1 // Intentionally 1pt (instead of 'Values.separatorThickness') layer.borderWidth = 1 // Intentionally 1pt (instead of 'Values.separatorThickness')
set(.height, to: self.height) set(.height, to: ReactionButton.height)
if showNumber || viewModel.number > 1 { numberLabel.isHidden = (!showNumber && viewModel.number <= 1)
let numberLabel = UILabel() }
numberLabel.font = .systemFont(ofSize: fontSize)
numberLabel.text = (viewModel.number < 1000 ? func update(with viewModel: ReactionViewModel, showNumber: Bool) {
"\(viewModel.number)" : _ = updating(with: viewModel, showNumber: showNumber)
String(format: "%.1f", Float(viewModel.number) / 1000) + "k" }
)
numberLabel.themeTextColor = .textPrimary func updating(with viewModel: ReactionViewModel, showNumber: Bool) -> ReactionButton {
stackView.addArrangedSubview(numberLabel) 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
} }
} }

View File

@ -8,6 +8,10 @@ final class InfoMessageCell: MessageCell {
private static let iconSize: CGFloat = 16 private static let iconSize: CGFloat = 16
private static let inset = Values.mediumSpacing private static let inset = Values.mediumSpacing
private var isHandlingLongPress: Bool = false
override var contextSnapshotView: UIView? { return label }
// MARK: - UI // MARK: - UI
private lazy var iconImageViewWidthConstraint = iconImageView.set(.width, to: InfoMessageCell.iconSize) 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(.right, to: .right, of: self, withInset: -InfoMessageCell.inset)
stackView.pin(.bottom, to: .bottom, 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 // MARK: - Updating
@ -90,4 +99,17 @@ final class InfoMessageCell: MessageCell {
override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) { 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
}
} }

View File

@ -10,8 +10,9 @@ public enum SwipeState {
} }
public class MessageCell: UITableViewCell { public class MessageCell: UITableViewCell {
weak var delegate: MessageCellDelegate?
var viewModel: MessageViewModel? var viewModel: MessageViewModel?
weak var delegate: MessageCellDelegate?
open var contextSnapshotView: UIView? { return nil }
// MARK: - Lifecycle // MARK: - Lifecycle

View File

@ -15,6 +15,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
var voiceMessageView: VoiceMessageView? var voiceMessageView: VoiceMessageView?
var audioStateChanged: ((TimeInterval, Bool) -> ())? var audioStateChanged: ((TimeInterval, Bool) -> ())?
override var contextSnapshotView: UIView? { return snContentView }
// Constraints // Constraints
private lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self) private lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self)
private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) 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 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 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 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 underBubbleStackViewIncomingLeadingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.leading, to: .leading, of: snContentView)
private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, 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 messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: reactionContainerView, withInset: 0) private lazy var underBubbleStackViewOutgoingTrailingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.trailing, to: .trailing, of: snContentView)
private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) private lazy var underBubbleStackViewNoHeightConstraint: NSLayoutConstraint = underBubbleStackView.set(.height, to: 0)
private lazy var messageStatusImageViewHeightConstraint = messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize)
private lazy var timerViewOutgoingMessageConstraint = timerView.pin(.left, to: .left, of: self, withInset: VisibleMessageCell.contactThreadHSpacing) 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) 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 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 = { private lazy var replyButton: UIView = {
let result = UIView() let result = UIView()
let size = VisibleMessageCell.replyButtonSize + 8 let size = VisibleMessageCell.replyButtonSize + 8
@ -128,6 +121,27 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
}() }()
private lazy var timerView: OWSMessageTimerView = OWSMessageTimerView() 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 // MARK: - Settings
@ -197,19 +211,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
timerView.center(.vertical, in: snContentView) timerView.center(.vertical, in: snContentView)
timerViewOutgoingMessageConstraint.isActive = true 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 // Reply button
addSubview(replyButton) addSubview(replyButton)
replyButton.addSubview(replyIconImageView) replyButton.addSubview(replyIconImageView)
@ -219,6 +220,20 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// Remaining constraints // Remaining constraints
authorLabel.pin(.left, to: .left, of: snContentView, withInset: VisibleMessageCell.authorLabelInset) 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() { override func setUpGestureRecognizers() {
@ -298,12 +313,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
lastSearchText: lastSearchText 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 // Author label
authorLabelTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0) authorLabelTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0)
authorLabel.isHidden = (cellViewModel.senderName == nil) authorLabel.isHidden = (cellViewModel.senderName == nil)
@ -315,27 +324,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
let authorLabelSize = authorLabel.sizeThatFits(authorLabelAvailableSpace) let authorLabelSize = authorLabel.sizeThatFits(authorLabelAvailableSpace)
authorLabelHeightConstraint.constant = (cellViewModel.senderName != nil ? authorLabelSize.height : 0) 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 // Timer
if if
let expiresStartedAtMs: Double = cellViewModel.expiresStartedAtMs, let expiresStartedAtMs: Double = cellViewModel.expiresStartedAtMs,
@ -367,6 +355,43 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
else { else {
addGestureRecognizer(panGestureRecognizer) 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( 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 ?? []) let reactions: OrderedDictionary<EmojiWithSkinTones, ReactionViewModel> = (cellViewModel.reactionInfo ?? [])
.reduce(into: OrderedDictionary()) { result, reactionInfo in .reduce(into: OrderedDictionary()) { result, reactionInfo in
guard let emoji: EmojiWithSkinTones = EmojiWithSkinTones(rawValue: reactionInfo.reaction.emoji) else { guard let emoji: EmojiWithSkinTones = EmojiWithSkinTones(rawValue: reactionInfo.reaction.emoji) else {
@ -626,9 +655,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
} }
} }
reactionContainerView.showingAllReactions = showExpandedReactions
reactionContainerView.update( reactionContainerView.update(
reactions.orderedValues, reactions.orderedValues,
maxWidth: maxWidth,
showingAllReactions: showExpandedReactions,
showNumbers: ( showNumbers: (
cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .closedGroup ||
cellViewModel.threadVariant == .openGroup cellViewModel.threadVariant == .openGroup
@ -752,7 +782,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
let location = gestureRecognizer.location(in: self) 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) let convertedLocation = reactionContainerView.convert(location, from: self)
for reactionView in reactionContainerView.reactionViews { for reactionView in reactionContainerView.reactionViews {
@ -774,7 +804,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
let location = gestureRecognizer.location(in: self) 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 // For open groups only attempt to start a conversation if the author has a blinded id
guard cellViewModel.threadVariant != .openGroup else { guard cellViewModel.threadVariant != .openGroup else {
guard SessionId.Prefix(from: cellViewModel.authorId) == .blinded else { return } guard SessionId.Prefix(from: cellViewModel.authorId) == .blinded else { return }
@ -793,11 +823,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
openGroupPublicKey: nil 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() UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
reply() reply()
} }
else if reactionContainerView.frame.contains(location) { else if reactionContainerView.bounds.contains(reactionContainerView.convert(location, from: self)) {
let convertedLocation = reactionContainerView.convert(location, from: self) let convertedLocation = reactionContainerView.convert(location, from: self)
for reactionView in reactionContainerView.reactionViews { 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() reactionContainerView.showAllEmojis()
delegate?.needsLayout(for: cellViewModel, expandingReactions: true) delegate?.needsLayout(for: cellViewModel, expandingReactions: true)
} }
@ -823,7 +853,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
delegate?.needsLayout(for: cellViewModel, expandingReactions: false) 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) delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer)
} }
} }
@ -966,11 +996,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
return CGSize(width: width, height: height) 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 screen: CGRect = UIScreen.main.bounds
let oppositeEdgePadding: CGFloat = (includingOppositeGutter ? gutterSize : contactThreadHSpacing)
switch cellViewModel.variant { switch cellViewModel.variant {
case .standardOutgoing: return (screen.width - contactThreadHSpacing - gutterSize) case .standardOutgoing:
return (screen.width - contactThreadHSpacing - oppositeEdgePadding)
case .standardIncoming, .standardIncomingDeleted: case .standardIncoming, .standardIncomingDeleted:
let isGroupThread = ( let isGroupThread = (
cellViewModel.threadVariant == .openGroup || cellViewModel.threadVariant == .openGroup ||
@ -978,7 +1011,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
) )
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing) let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)
return (screen.width - leftGutterSize - gutterSize) return (screen.width - leftGutterSize - oppositeEdgePadding)
default: preconditionFailure() default: preconditionFailure()
} }

View File

@ -28,8 +28,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
// MARK: - Variables // MARK: - Variables
private let storage: Storage private let dependencies: Dependencies
private let scheduler: ValueObservationScheduler
private let threadId: String private let threadId: String
private let config: DisappearingMessagesConfiguration private let config: DisappearingMessagesConfiguration
private var storedSelection: TimeInterval private var storedSelection: TimeInterval
@ -38,13 +37,11 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
// MARK: - Initialization // MARK: - Initialization
init( init(
storage: Storage = Storage.shared, dependencies: Dependencies = Dependencies(),
scheduling scheduler: ValueObservationScheduler = Storage.defaultPublisherScheduler,
threadId: String, threadId: String,
config: DisappearingMessagesConfiguration config: DisappearingMessagesConfiguration
) { ) {
self.storage = storage self.dependencies = dependencies
self.scheduler = scheduler
self.threadId = threadId self.threadId = threadId
self.config = config self.config = config
self.storedSelection = (config.isEnabled ? config.durationSeconds : 0) self.storedSelection = (config.isEnabled ? config.durationSeconds : 0)
@ -133,7 +130,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
] ]
} }
.removeDuplicates() .removeDuplicates()
.publisher(in: storage, scheduling: scheduler) .publisher(in: dependencies.storage, scheduling: dependencies.scheduler)
// MARK: - Functions // MARK: - Functions
@ -152,7 +149,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
guard self.config != updatedConfig else { return } 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 { guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
return return
} }

View File

@ -46,6 +46,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
// MARK: - Variables // MARK: - Variables
private let dependencies: Dependencies
private let threadId: String private let threadId: String
private let threadVariant: SessionThread.Variant private let threadVariant: SessionThread.Variant
private let didTriggerSearch: () -> () private let didTriggerSearch: () -> ()
@ -54,13 +55,19 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
// MARK: - Initialization // 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.threadId = threadId
self.threadVariant = threadVariant self.threadVariant = threadVariant
self.didTriggerSearch = didTriggerSearch self.didTriggerSearch = didTriggerSearch
self.oldDisplayName = (threadVariant != .contact ? self.oldDisplayName = (threadVariant != .contact ?
nil : nil :
Storage.shared.read { db in dependencies.storage.read { db in
try Profile try Profile
.filter(id: threadId) .filter(id: threadId)
.select(.nickname) .select(.nickname)
@ -73,55 +80,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
// MARK: - Navigation // MARK: - Navigation
lazy var navState: AnyPublisher<NavState, Never> = { lazy var navState: AnyPublisher<NavState, Never> = {
Publishers isEditing
.MergeMany( .map { isEditing in (isEditing ? .editing : .standard) }
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()
)
.removeDuplicates() .removeDuplicates()
.prepend(.standard) // Initial value .prepend(.standard) // Initial value
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -139,7 +99,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
id: .cancel, id: .cancel,
systemItem: .cancel, systemItem: .cancel,
accessibilityIdentifier: "Cancel button" accessibilityIdentifier: "Cancel button"
) ) { [weak self] in
self?.setIsEditing(false)
self?.editedDisplayName = self?.oldDisplayName
}
] ]
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -147,7 +110,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
override var rightNavItems: AnyPublisher<[NavItem]?, Never> { override var rightNavItems: AnyPublisher<[NavItem]?, Never> {
navState 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 // Only show the 'Edit' button if it's a contact thread
guard self?.threadVariant == .contact else { return [] } guard self?.threadVariant == .contact else { return [] }
@ -158,7 +121,29 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
id: .done, id: .done,
systemItem: .done, systemItem: .done,
accessibilityIdentifier: "Done button" 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: case .standard:
@ -167,7 +152,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
id: .edit, id: .edit,
systemItem: .edit, systemItem: .edit,
accessibilityIdentifier: "Edit button" 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`) /// 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 /// 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 private lazy var _observableSettingsData: ObservableData = ValueObservation
.trackingConstantRegion { [weak self, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in .trackingConstantRegion { [weak self, dependencies, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey) .conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
.fetchOne(db) .fetchOne(db)
@ -387,7 +372,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
cancelStyle: .alert_text cancelStyle: .alert_text
), ),
onTap: { [weak self] in onTap: { [weak self] in
Storage.shared.writeAsync { db in dependencies.storage.writeAsync { db in
try MessageSender.leave(db, groupPublicKey: threadId) try MessageSender.leave(db, groupPublicKey: threadId)
} }
} }
@ -435,7 +420,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
onTap: { onTap: {
let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true) let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true)
Storage.shared.writeAsync { db in dependencies.storage.writeAsync { db in
try SessionThread try SessionThread
.filter(id: threadId) .filter(id: threadId)
.updateAll( .updateAll(
@ -465,15 +450,19 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
), ),
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).mute", accessibilityIdentifier: "\(ThreadSettingsViewModel.self).mute",
onTap: { onTap: {
let newValue: Bool = !(threadViewModel.threadMutedUntilTimestamp != nil) dependencies.storage.writeAsync { db in
let currentValue: TimeInterval? = try SessionThread
Storage.shared.writeAsync { db in .filter(id: threadId)
.select(.mutedUntilTimestamp)
.asRequest(of: TimeInterval.self)
.fetchOne(db)
try SessionThread try SessionThread
.filter(id: threadId) .filter(id: threadId)
.updateAll( .updateAll(
db, db,
SessionThread.Columns.mutedUntilTimestamp.set( SessionThread.Columns.mutedUntilTimestamp.set(
to: (newValue ? to: (currentValue == nil ?
Date.distantFuture.timeIntervalSince1970 : Date.distantFuture.timeIntervalSince1970 :
nil nil
) )
@ -538,7 +527,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
] ]
} }
.removeDuplicates() .removeDuplicates()
.publisher(in: Storage.shared) .publisher(in: dependencies.storage, scheduling: dependencies.scheduler)
// MARK: - Functions // MARK: - Functions
@ -575,7 +564,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
private func addUsersToOpenGoup(selectedUsers: Set<String>) { private func addUsersToOpenGoup(selectedUsers: Set<String>) {
let threadId: String = self.threadId 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 } guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { return }
let urlString: String = "\(openGroup.server)/\(openGroup.roomToken)?public_key=\(openGroup.publicKey)" 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 } guard oldBlockedState != isBlocked else { return }
Storage.shared.writeAsync( dependencies.storage.writeAsync(
updates: { db in updates: { db in
try Contact try Contact
.fetchOrCreate(db, id: threadId) .fetchOrCreate(db, id: threadId)

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -476,6 +476,7 @@
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; "EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; "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_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/ /* New conversation screen*/
"vc_new_conversation_title" = "New Conversation"; "vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create"; "CREATE_GROUP_BUTTON_TITLE" = "Create";

View File

@ -63,66 +63,8 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
// MARK: - Navigation // MARK: - Navigation
lazy var navState: AnyPublisher<NavState, Never> = { lazy var navState: AnyPublisher<NavState, Never> = {
Publishers isEditing
.MergeMany( .map { isEditing in (isEditing ? .editing : .standard) }
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()
)
.removeDuplicates() .removeDuplicates()
.prepend(.standard) // Initial value .prepend(.standard) // Initial value
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -149,7 +91,10 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
id: .cancel, id: .cancel,
systemItem: .cancel, systemItem: .cancel,
accessibilityIdentifier: "Cancel button" 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, id: .done,
systemItem: .done, systemItem: .done,
accessibilityIdentifier: "Done button" 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
)
}
] ]
} }
} }

View File

@ -1344,9 +1344,12 @@ public enum OpenGroupAPI {
let method: String = (request.httpMethod ?? "GET") let method: String = (request.httpMethod ?? "GET")
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970)) let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
let nonce: Data = Data(dependencies.nonceGenerator16.nonce()) let nonce: Data = Data(dependencies.nonceGenerator16.nonce())
let serverPublicKeyData: Data = Data(hex: serverPublicKey)
guard let serverPublicKeyData: Data = serverPublicKey.dataFromHex() else { return nil } guard
guard let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes else { return nil } !serverPublicKeyData.isEmpty,
let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes
else { return nil }
/// Get a hash of any body content /// Get a hash of any body content
let bodyHash: Bytes? = { let bodyHash: Bytes? = {

View File

@ -1129,6 +1129,7 @@ extension OpenGroupManager {
onionApi: OnionRequestAPIType.Type? = nil, onionApi: OnionRequestAPIType.Type? = nil,
generalCache: Atomic<GeneralCacheType>? = nil, generalCache: Atomic<GeneralCacheType>? = nil,
storage: Storage? = nil, storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil, sodium: SodiumType? = nil,
box: BoxType? = nil, box: BoxType? = nil,
genericHash: GenericHashType? = nil, genericHash: GenericHashType? = nil,
@ -1146,6 +1147,7 @@ extension OpenGroupManager {
onionApi: onionApi, onionApi: onionApi,
generalCache: generalCache, generalCache: generalCache,
storage: storage, storage: storage,
scheduler: scheduler,
sodium: sodium, sodium: sodium,
box: box, box: box,
genericHash: genericHash, genericHash: genericHash,

View File

@ -510,10 +510,12 @@ public extension MessageViewModel {
// Interaction Info // Interaction Info
let targetId: Int64 = (isTypingIndicator == true ? let targetId: Int64 = {
MessageViewModel.typingIndicatorId : guard isTypingIndicator != true else { return MessageViewModel.typingIndicatorId }
MessageViewModel.genericId guard cellType != .dateHeader else { return -timestampMs }
)
return MessageViewModel.genericId
}()
self.rowId = targetId self.rowId = targetId
self.id = targetId self.id = targetId
self.variant = variant self.variant = variant

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import GRDB
import Sodium import Sodium
import SessionSnodeKit import SessionSnodeKit
import SessionUtilitiesKit import SessionUtilitiesKit
@ -66,6 +67,7 @@ public class SMKDependencies: Dependencies {
onionApi: OnionRequestAPIType.Type? = nil, onionApi: OnionRequestAPIType.Type? = nil,
generalCache: Atomic<GeneralCacheType>? = nil, generalCache: Atomic<GeneralCacheType>? = nil,
storage: Storage? = nil, storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil, sodium: SodiumType? = nil,
box: BoxType? = nil, box: BoxType? = nil,
genericHash: GenericHashType? = nil, genericHash: GenericHashType? = nil,
@ -90,6 +92,7 @@ public class SMKDependencies: Dependencies {
super.init( super.init(
generalCache: generalCache, generalCache: generalCache,
storage: storage, storage: storage,
scheduler: scheduler,
standardUserDefaults: standardUserDefaults, standardUserDefaults: standardUserDefaults,
date: date date: date
) )

View File

@ -25,8 +25,9 @@ extension Sodium {
/// 64-byte blake2b hash then reduce to get the blinding factor /// 64-byte blake2b hash then reduce to get the blinding factor
public func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? { public func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? {
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest()) /// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
guard let serverPubKeyData: Data = serverPublicKey.dataFromHex() else { return nil } let serverPubKeyData: Data = Data(hex: serverPublicKey)
guard let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
guard !serverPubKeyData.isEmpty, let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
return nil return nil
} }

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import GRDB
import SessionSnodeKit import SessionSnodeKit
import SessionUtilitiesKit import SessionUtilitiesKit
@ -11,6 +12,7 @@ extension SMKDependencies {
onionApi: OnionRequestAPIType.Type? = nil, onionApi: OnionRequestAPIType.Type? = nil,
generalCache: Atomic<GeneralCacheType>? = nil, generalCache: Atomic<GeneralCacheType>? = nil,
storage: Storage? = nil, storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil, sodium: SodiumType? = nil,
box: BoxType? = nil, box: BoxType? = nil,
genericHash: GenericHashType? = nil, genericHash: GenericHashType? = nil,
@ -26,6 +28,7 @@ extension SMKDependencies {
onionApi: (onionApi ?? self._onionApi.wrappedValue), onionApi: (onionApi ?? self._onionApi.wrappedValue),
generalCache: (generalCache ?? self._generalCache.wrappedValue), generalCache: (generalCache ?? self._generalCache.wrappedValue),
storage: (storage ?? self._storage.wrappedValue), storage: (storage ?? self._storage.wrappedValue),
scheduler: (scheduler ?? self._scheduler.wrappedValue),
sodium: (sodium ?? self._sodium.wrappedValue), sodium: (sodium ?? self._sodium.wrappedValue),
box: (box ?? self._box.wrappedValue), box: (box ?? self._box.wrappedValue),
genericHash: (genericHash ?? self._genericHash.wrappedValue), genericHash: (genericHash ?? self._genericHash.wrappedValue),

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import GRDB
import SessionSnodeKit import SessionSnodeKit
import SessionUtilitiesKit import SessionUtilitiesKit
@ -12,6 +13,7 @@ extension OpenGroupManager.OGMDependencies {
onionApi: OnionRequestAPIType.Type? = nil, onionApi: OnionRequestAPIType.Type? = nil,
generalCache: Atomic<GeneralCacheType>? = nil, generalCache: Atomic<GeneralCacheType>? = nil,
storage: Storage? = nil, storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil, sodium: SodiumType? = nil,
box: BoxType? = nil, box: BoxType? = nil,
genericHash: GenericHashType? = nil, genericHash: GenericHashType? = nil,
@ -28,6 +30,7 @@ extension OpenGroupManager.OGMDependencies {
onionApi: (onionApi ?? self._onionApi.wrappedValue), onionApi: (onionApi ?? self._onionApi.wrappedValue),
generalCache: (generalCache ?? self._generalCache.wrappedValue), generalCache: (generalCache ?? self._generalCache.wrappedValue),
storage: (storage ?? self._storage.wrappedValue), storage: (storage ?? self._storage.wrappedValue),
scheduler: (scheduler ?? self._scheduler.wrappedValue),
sodium: (sodium ?? self._sodium.wrappedValue), sodium: (sodium ?? self._sodium.wrappedValue),
box: (box ?? self._box.wrappedValue), box: (box ?? self._box.wrappedValue),
genericHash: (genericHash ?? self._genericHash.wrappedValue), genericHash: (genericHash ?? self._genericHash.wrappedValue),

View File

@ -14,8 +14,8 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
override func spec() { override func spec() {
var mockStorage: Storage! var mockStorage: Storage!
var dataChangeCancellable: AnyCancellable? var cancellables: [AnyCancellable] = []
var otherCancellables: [AnyCancellable] = [] var dependencies: Dependencies!
var viewModel: ThreadDisappearingMessagesViewModel! var viewModel: ThreadDisappearingMessagesViewModel!
describe("a ThreadDisappearingMessagesViewModel") { describe("a ThreadDisappearingMessagesViewModel") {
@ -31,6 +31,10 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
SNUIKit.migrations() SNUIKit.migrations()
] ]
) )
dependencies = Dependencies(
storage: mockStorage,
scheduler: .immediate
)
mockStorage.write { db in mockStorage.write { db in
try SessionThread( try SessionThread(
id: "TestId", id: "TestId",
@ -38,26 +42,26 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
).insert(db) ).insert(db)
} }
viewModel = ThreadDisappearingMessagesViewModel( viewModel = ThreadDisappearingMessagesViewModel(
storage: mockStorage, dependencies: dependencies,
scheduling: .immediate,
threadId: "TestId", threadId: "TestId",
config: DisappearingMessagesConfiguration.defaultWith("TestId") config: DisappearingMessagesConfiguration.defaultWith("TestId")
) )
dataChangeCancellable = viewModel.observableSettingsData cancellables.append(
.receiveOnMain(immediately: true) viewModel.observableSettingsData
.sink( .receiveOnMain(immediately: true)
receiveCompletion: { _ in }, .sink(
receiveValue: { viewModel.updateSettings($0) } receiveCompletion: { _ in },
) receiveValue: { viewModel.updateSettings($0) }
)
)
} }
afterEach { afterEach {
dataChangeCancellable?.cancel() cancellables.forEach { $0.cancel() }
otherCancellables.forEach { $0.cancel() }
mockStorage = nil mockStorage = nil
dataChangeCancellable = nil cancellables = []
otherCancellables = [] dependencies = nil
viewModel = nil viewModel = nil
} }
@ -118,17 +122,18 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
_ = try config.saved(db) _ = try config.saved(db)
} }
viewModel = ThreadDisappearingMessagesViewModel( viewModel = ThreadDisappearingMessagesViewModel(
storage: mockStorage, dependencies: dependencies,
scheduling: .immediate,
threadId: "TestId", threadId: "TestId",
config: config config: config
) )
dataChangeCancellable = viewModel.observableSettingsData cancellables.append(
.receiveOnMain(immediately: true) viewModel.observableSettingsData
.sink( .receiveOnMain(immediately: true)
receiveCompletion: { _ in }, .sink(
receiveValue: { viewModel.updateSettings($0) } receiveCompletion: { _ in },
) receiveValue: { viewModel.updateSettings($0) }
)
)
expect(viewModel.settingsData.first?.elements.first) expect(viewModel.settingsData.first?.elements.first)
.to( .to(
@ -165,7 +170,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
it("has no right bar button") { it("has no right bar button") {
var items: [ParentType.NavItem]? var items: [ParentType.NavItem]?
otherCancellables.append( cancellables.append(
viewModel.rightNavItems viewModel.rightNavItems
.receiveOnMain(immediately: true) .receiveOnMain(immediately: true)
.sink( .sink(
@ -181,7 +186,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
var items: [ParentType.NavItem]? var items: [ParentType.NavItem]?
beforeEach { beforeEach {
otherCancellables.append( cancellables.append(
viewModel.rightNavItems viewModel.rightNavItems
.receiveOnMain(immediately: true) .receiveOnMain(immediately: true)
.sink( .sink(
@ -208,7 +213,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
it("dismisses the screen") { it("dismisses the screen") {
var didDismissScreen: Bool = false var didDismissScreen: Bool = false
otherCancellables.append( cancellables.append(
viewModel.dismissScreen viewModel.dismissScreen
.receiveOnMain(immediately: true) .receiveOnMain(immediately: true)
.sink( .sink(

View File

@ -6,7 +6,7 @@ import GRDB
import PromiseKit import PromiseKit
import SignalCoreKit import SignalCoreKit
public final class Storage { open class Storage {
private static let dbFileName: String = "Session.sqlite" private static let dbFileName: String = "Session.sqlite"
private static let keychainService: String = "TSKeyChainService" private static let keychainService: String = "TSKeyChainService"
private static let dbCipherKeySpecKey: String = "GRDBDatabaseCipherKeySpec" private static let dbCipherKeySpecKey: String = "GRDBDatabaseCipherKeySpec"
@ -305,17 +305,17 @@ public final class Storage {
// MARK: - Functions // 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 } guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
return try? dbWriter.write(updates) 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 }) 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 } guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return }
dbWriter.asyncWrite( 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 } guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
return try? dbWriter.read(value) 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> { @discardableResult func writeAsync<T>(updates: @escaping (Database) throws -> Promise<T>) -> Promise<T> {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
return Promise(error: StorageError.databaseInvalid) return Promise(error: StorageError.databaseInvalid)

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import GRDB
open class Dependencies { open class Dependencies {
public var _generalCache: Atomic<Atomic<GeneralCacheType>?> public var _generalCache: Atomic<Atomic<GeneralCacheType>?>
@ -15,6 +16,12 @@ open class Dependencies {
set { _storage.mutate { $0 = newValue } } 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: Atomic<UserDefaultsType?>
public var standardUserDefaults: UserDefaultsType { public var standardUserDefaults: UserDefaultsType {
get { Dependencies.getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } } get { Dependencies.getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } }
@ -32,11 +39,13 @@ open class Dependencies {
public init( public init(
generalCache: Atomic<GeneralCacheType>? = nil, generalCache: Atomic<GeneralCacheType>? = nil,
storage: Storage? = nil, storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
standardUserDefaults: UserDefaultsType? = nil, standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil date: Date? = nil
) { ) {
_generalCache = Atomic(generalCache) _generalCache = Atomic(generalCache)
_storage = Atomic(storage) _storage = Atomic(storage)
_scheduler = Atomic(scheduler)
_standardUserDefaults = Atomic(standardUserDefaults) _standardUserDefaults = Atomic(standardUserDefaults)
_date = Atomic(date) _date = Atomic(date)
} }
@ -52,12 +61,4 @@ open class Dependencies {
return value 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)
} }

View File

@ -44,20 +44,6 @@ public extension String {
return localizedString 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>] { func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
var ranges: [Range<Index>] = [] var ranges: [Range<Index>] = []

View File

@ -22,7 +22,11 @@ public class ToastController: ToastViewDelegate {
// MARK: Public // MARK: Public
public func presentToastView(fromBottomOfView view: UIView, inset: CGFloat) { public func presentToastView(
fromBottomOfView view: UIView,
inset: CGFloat,
duration: DispatchTimeInterval = .milliseconds(1500)
) {
Logger.debug("") Logger.debug("")
toastView.alpha = 0 toastView.alpha = 0
view.addSubview(toastView) view.addSubview(toastView)
@ -46,7 +50,7 @@ public class ToastController: ToastViewDelegate {
self.toastView.alpha = 1 self.toastView.alpha = 1
} }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) { DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
// intentional strong reference to self. // intentional strong reference to self.
// As with an AlertController, the caller likely expects toast to // As with an AlertController, the caller likely expects toast to
// be presented and dismissed without maintaining a strong reference to ToastController // be presented and dismissed without maintaining a strong reference to ToastController

View 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
}
}

View File

@ -6,16 +6,16 @@ import Curve25519Kit
extension Box.KeyPair: Mocked { extension Box.KeyPair: Mocked {
static var mockValue: Box.KeyPair = Box.KeyPair( static var mockValue: Box.KeyPair = Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, publicKey: Data(hex: TestConstants.publicKey).bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes secretKey: Data(hex: TestConstants.edSecretKey).bytes
) )
} }
extension ECKeyPair: Mocked { extension ECKeyPair: Mocked {
static var mockValue: Self { static var mockValue: Self {
try! Self.init( try! Self.init(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, publicKeyData: Data(hex: TestConstants.publicKey),
privateKeyData: Data.data(fromHex: TestConstants.privateKey)! privateKeyData: Data(hex: TestConstants.privateKey)
) )
} }
} }

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