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