Merge branch 'theming' into ipad-landscape-support

This commit is contained in:
Ryan Zhao 2022-10-06 11:14:31 +11:00
commit 671c5f6ada
60 changed files with 1292 additions and 1274 deletions

View file

@ -573,6 +573,13 @@
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; };
FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */; };
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; };
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; };
FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; };
FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; };
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; };
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; };
FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7552553A3AB00C340D1 /* VisibleMessage+Quote.swift */; };
FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5FB2554B0A000555489 /* MessageReceiver.swift */; };
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF224255B6D5D007E1867 /* SignalAttachment.swift */; };
@ -599,6 +606,11 @@
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; };
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */; };
FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */; };
@ -1678,8 +1690,10 @@
FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUKLegacy.swift; sourceTree = "<group>"; };
FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = "<group>"; };
FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = "<group>"; };
FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+ClassicDark.swift"; sourceTree = "<group>"; };
FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Colors.swift"; sourceTree = "<group>"; };
@ -3374,7 +3388,7 @@
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */,
C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */,
FD83B9BC27CF2215005E1583 /* SharedTest */,
FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */,
FD71160A28D00BAE00B47552 /* SessionTests */,
FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */,
FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */,
@ -3897,15 +3911,18 @@
path = General;
sourceTree = "<group>";
};
FD83B9BC27CF2215005E1583 /* SharedTest */ = {
FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */ = {
isa = PBXGroup;
children = (
FDC290A527D860CE005DAE71 /* Mock.swift */,
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */,
FD83B9BD27CF2243005E1583 /* TestConstants.swift */,
FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */,
FD078E4727E02561000769AF /* CommonMockedExtensions.swift */,
FD23EA6028ED0B260058676E /* CombineExtensions.swift */,
FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */,
);
path = SharedTest;
path = _SharedTestUtilities;
sourceTree = "<group>";
};
FD83B9C127CF33EE005E1583 /* Models */ = {
@ -4052,7 +4069,6 @@
isa = PBXGroup;
children = (
FDC438BC27BB2AB400C60D73 /* Mockable.swift */,
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */,
FD859EF327C2F49200510D0C /* MockSodium.swift */,
FD3C906E27E43E8700CD579F /* MockBox.swift */,
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */,
@ -5752,8 +5768,15 @@
buildActionMask = 2147483647;
files = (
FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */,
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */,
FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */,
FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */,
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */,
FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */,
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */,
FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */,
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5761,10 +5784,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
);
@ -5789,6 +5815,7 @@
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */,
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */,
@ -5800,6 +5827,7 @@
FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */,
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */,
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */,
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */,
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
@ -6004,7 +6032,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 375;
CURRENT_PROJECT_VERSION = 376;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6029,7 +6057,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.0;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6077,7 +6105,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 375;
CURRENT_PROJECT_VERSION = 376;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6107,7 +6135,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.0;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6143,7 +6171,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 375;
CURRENT_PROJECT_VERSION = 376;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6166,7 +6194,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6217,7 +6245,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 375;
CURRENT_PROJECT_VERSION = 376;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6245,7 +6273,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7145,7 +7173,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 375;
CURRENT_PROJECT_VERSION = 376;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7184,7 +7212,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.0;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7217,7 +7245,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 375;
CURRENT_PROJECT_VERSION = 376;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7256,7 +7284,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.0;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View file

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

View file

@ -46,27 +46,18 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
return result
}()
private lazy var fadeView: UIView = {
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64)
let result = UIView()
var frame = UIScreen.main.bounds
frame.size.height = height
let layer = CAGradientLayer()
layer.frame = frame
result.layer.insertSublayer(layer, at: 0)
private lazy var fadeView: GradientView = {
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top)
.map { $0 + Values.veryLargeSpacing })
.defaulting(to: 64)
let result: GradientView = GradientView()
result.themeBackgroundGradient = [
.value(.backgroundPrimary, alpha: 0.4),
.value(.backgroundPrimary, alpha: 0)
]
result.set(.height, to: height)
ThemeManager.onThemeChange(observer: result) { [weak layer] theme, _ in
guard let backgroundPrimary: UIColor = theme.color(for: .backgroundPrimary) else { return }
layer?.colors = [
backgroundPrimary.withAlphaComponent(0.4).cgColor,
backgroundPrimary.withAlphaComponent(0).cgColor
]
}
return result
}()

View file

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

View file

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

View file

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

View file

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

View file

@ -713,10 +713,23 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
let newSectionIndex: Int = updatedData.firstIndex(where: { $0.model == .messages }),
let newFirstItemIndex: Int = updatedData[newSectionIndex].elements
.firstIndex(where: { item -> Bool in
item.id == self.viewModel.interactionData[oldSectionIndex].elements.first?.id
// Since the first item is probably a `DateHeaderCell` (which would likely
// be removed when inserting items above it) we check if the id matches
// either the first or second item
let messages: [MessageViewModel] = self.viewModel
.interactionData[oldSectionIndex]
.elements
return (
item.id == messages[safe: 0]?.id ||
item.id == messages[safe: 1]?.id
)
}),
let firstVisibleIndexPath: IndexPath = self.tableView.indexPathsForVisibleRows?
.filter({ $0.section == oldSectionIndex })
.filter({
$0.section == oldSectionIndex &&
self.viewModel.interactionData[$0.section].elements[$0.row].cellType != .dateHeader
})
.sorted()
.first,
let newVisibleIndex: Int = updatedData[newSectionIndex].elements
@ -730,7 +743,9 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
return ItemChangeInfo(
isInsertAtTop: (
newSectionIndex > oldSectionIndex ||
newFirstItemIndex > 0
// Note: Using `1` here instead of `0` as the first item will generally
// be a `DateHeaderCell` instead of a message
newFirstItemIndex > 1
),
firstIndexIsVisible: (firstVisibleIndexPath.row == 0),
visibleIndexPath: IndexPath(row: newVisibleIndex, section: newSectionIndex),

View file

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

View file

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

View file

@ -2,11 +2,17 @@
import UIKit
import SessionUIKit
import SessionUtilitiesKit
import SignalUtilitiesKit
final class ReactionContainerView: UIView {
var showingAllReactions = false
private var showNumbers = true
private static let arrowSize: CGSize = CGSize(width: 15, height: 13)
private static let arrowSpacing: CGFloat = Values.verySmallSpacing
private var maxWidth: CGFloat = 0
private var collapsedCount: Int = 0
private var showingAllReactions: Bool = false
private var showNumbers: Bool = true
private var maxEmojisPerLine = UIDevice.current.isIPad ? 10 : (isIPhone6OrSmaller ? 5 : 6)
private var oldSize: CGSize = .zero
@ -15,6 +21,16 @@ final class ReactionContainerView: UIView {
// MARK: - UI
private var collapseTextLabelRightConstraint: NSLayoutConstraint?
private let dummyReactionButton: ReactionButton = ReactionButton(
viewModel: ReactionViewModel(
emoji: EmojiWithSkinTones(baseEmoji: .a, skinTones: nil),
number: 0,
showBorder: false
)
)
private lazy var mainStackView: UIStackView = {
let result: UIStackView = UIStackView(arrangedSubviews: [ reactionContainerView, collapseButton ])
result.axis = .vertical
@ -24,6 +40,8 @@ final class ReactionContainerView: UIView {
return result
}()
var expandButton: ExpandingReactionButton?
private lazy var reactionContainerView: UIStackView = {
let result: UIStackView = UIStackView()
result.axis = .vertical
@ -33,34 +51,36 @@ final class ReactionContainerView: UIView {
return result
}()
var expandButton: ExpandingReactionButton?
var collapseButton: UIStackView = {
let arrow = UIImageView(
lazy var collapseButton: UIView = {
let arrow: UIImageView = UIImageView(
image: UIImage(named: "ic_chevron_up")?
.resizedImage(to: CGSize(width: 15, height: 13))?
.resizedImage(to: ReactionContainerView.arrowSize)?
.withRenderingMode(.alwaysTemplate)
)
arrow.themeTintColor = .textPrimary
let textLabel: UILabel = UILabel()
textLabel.setContentHuggingPriority(.required, for: .vertical)
textLabel.setContentHuggingPriority(.required, for: .horizontal)
textLabel.setContentCompressionResistancePriority(.required, for: .vertical)
textLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
textLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
textLabel.text = "EMOJI_REACTS_SHOW_LESS".localized()
textLabel.themeTextColor = .textPrimary
let leftSpacer: UIView = UIView.hStretchingSpacer()
let rightSpacer: UIView = UIView.hStretchingSpacer()
let result: UIStackView = UIStackView(arrangedSubviews: [
leftSpacer,
arrow,
textLabel,
rightSpacer
])
result.isLayoutMarginsRelativeArrangement = true
result.spacing = Values.verySmallSpacing
result.alignment = .center
let result: UIView = UIView()
result.isHidden = true
rightSpacer.set(.width, to: .width, of: leftSpacer)
result.addSubview(arrow)
result.addSubview(textLabel)
arrow.pin(.top, to: .top, of: result)
arrow.pin(.leading, to: .leading, of: result)
arrow.pin(.bottom, to: .bottom, of: result)
textLabel.pin(.top, to: .top, of: result)
textLabel.pin(.leading, to: .trailing, of: arrow, withInset: ReactionContainerView.arrowSpacing)
collapseTextLabelRightConstraint = textLabel.pin(.trailing, to: .trailing, of: result)
textLabel.pin(.bottom, to: .bottom, of: result)
return result
}()
@ -84,123 +104,181 @@ final class ReactionContainerView: UIView {
addSubview(mainStackView)
mainStackView.pin(to: self)
collapseButton.set(.width, to: .width, of: mainStackView)
reactionContainerView.set(.width, to: .width, of: mainStackView)
}
override func layoutSubviews() {
super.layoutSubviews()
// Note: We update the 'collapseButton.layoutMargins' to try to make the "show less"
// Note: We update the 'collapseTextLabelRightConstraint' to try to make the "show less"
// button appear horizontally centered (if we don't do this it gets offset to one side)
guard frame != CGRect.zero, frame.size != oldSize else { return }
collapseButton.layoutMargins = UIEdgeInsets(
top: 0,
leading: -frame.minX,
bottom: 0,
trailing: -((superview?.frame.width ?? 0) - frame.maxX)
)
var targetSuperview: UIView? = {
var result: UIView? = self.superview
while result != nil, result?.isKind(of: UITableViewCell.self) != true {
result = result?.superview
}
return result
}()
if let targetSuperview: UIView = targetSuperview {
let parentWidth: CGFloat = targetSuperview.bounds.width
let frameInParent: CGRect = targetSuperview.convert(self.bounds, from: self)
let centeredWidth: CGFloat = (parentWidth - (frameInParent.minX * 2))
let diff: CGFloat = (frameInParent.width - centeredWidth)
collapseTextLabelRightConstraint?.constant = -(
diff +
((ReactionContainerView.arrowSize.width + ReactionContainerView.arrowSpacing) / 2)
)
}
oldSize = frame.size
}
public func update(_ reactions: [ReactionViewModel], showNumbers: Bool) {
public func update(
_ reactions: [ReactionViewModel],
maxWidth: CGFloat,
showingAllReactions: Bool,
showNumbers: Bool
) {
self.reactions = reactions
self.maxWidth = maxWidth
self.collapsedCount = {
var numReactions: Int = 0
var runningWidth: CGFloat = 0
let estimatedExpandingButtonWidth: CGFloat = 52
let itemSpacing: CGFloat = self.reactionContainerView.spacing
for reaction in reactions {
let reactionViewWidth: CGFloat = dummyReactionButton
.updating(with: reaction, showNumber: showNumbers)
.systemLayoutSizeFitting(CGSize(width: maxWidth, height: 9999))
.width
let estimatedFullWidth: CGFloat = (
runningWidth +
(reactionViewWidth + itemSpacing) +
estimatedExpandingButtonWidth
)
if estimatedFullWidth >= maxWidth {
break
}
runningWidth += (reactionViewWidth + itemSpacing)
numReactions += 1
}
return numReactions
}()
self.showNumbers = showNumbers
self.reactionViews = []
self.reactionContainerView.arrangedSubviews.forEach { $0.removeFromSuperview() }
prepareForUpdate()
if showingAllReactions {
updateAllReactions()
// Generate the lines of reactions (if the 'collapsedCount' matches the total number of
// reactions then just show them app)
if showingAllReactions || self.collapsedCount >= reactions.count {
self.updateAllReactions(reactions, maxWidth: maxWidth, showNumbers: showNumbers)
}
else {
updateCollapsedReactions(reactions)
self.updateCollapsedReactions(reactions, maxWidth: maxWidth, showNumbers: showNumbers)
}
// Just in case we couldn't show everything for some reason update this based on the
// internal logic
self.collapseButton.isHidden = (self.reactionContainerView.arrangedSubviews.count <= 1)
self.showingAllReactions = !self.collapseButton.isHidden
self.layoutIfNeeded()
}
private func updateCollapsedReactions(_ reactions: [ReactionViewModel]) {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = Values.smallSpacing
stackView.alignment = .center
private func createLineStackView() -> UIStackView {
let result: UIStackView = UIStackView()
result.axis = .horizontal
result.spacing = Values.smallSpacing
result.alignment = .center
result.set(.height, to: ReactionButton.height)
var displayedReactions: [ReactionViewModel]
var expandButtonReactions: [EmojiWithSkinTones]
return result
}
private func updateCollapsedReactions(
_ reactions: [ReactionViewModel],
maxWidth: CGFloat,
showNumbers: Bool
) {
guard !reactions.isEmpty else { return }
if reactions.count > maxEmojisPerLine {
displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)])
expandButtonReactions = Array(reactions[(maxEmojisPerLine - 2)...maxEmojisPerLine])
.map { $0.emoji }
}
else {
displayedReactions = reactions
expandButtonReactions = []
}
let maxSize: CGSize = CGSize(width: maxWidth, height: 9999)
let stackView: UIStackView = createLineStackView()
let displayedReactions: [ReactionViewModel] = Array(reactions.prefix(upTo: self.collapsedCount))
let expandButtonReactions: [EmojiWithSkinTones] = reactions
.suffix(from: self.collapsedCount)
.prefix(3)
.map { $0.emoji }
for reaction in displayedReactions {
let reactionView = ReactionButton(viewModel: reaction, showNumber: showNumbers)
let reactionViewWidth: CGFloat = reactionView.systemLayoutSizeFitting(maxSize).width
stackView.addArrangedSubview(reactionView)
reactionViews.append(reactionView)
reactionView.set(.width, to: reactionViewWidth)
}
if expandButtonReactions.count > 0 {
let expandButton: ExpandingReactionButton = ExpandingReactionButton(emojis: expandButtonReactions)
stackView.addArrangedSubview(expandButton)
self.expandButton = {
guard !expandButtonReactions.isEmpty else { return nil }
let result: ExpandingReactionButton = ExpandingReactionButton(emojis: expandButtonReactions)
stackView.addArrangedSubview(result)
self.expandButton = expandButton
}
else {
expandButton = nil
}
return result
}()
reactionContainerView.addArrangedSubview(stackView)
}
private func updateAllReactions() {
var reactions = self.reactions
var numberOfLines = 0
private func updateAllReactions(
_ reactions: [ReactionViewModel],
maxWidth: CGFloat,
showNumbers: Bool
) {
guard !reactions.isEmpty else { return }
while reactions.count > 0 {
var line: [ReactionViewModel] = []
let maxSize: CGSize = CGSize(width: maxWidth, height: 9999)
var lineStackView: UIStackView = createLineStackView()
reactionContainerView.addArrangedSubview(lineStackView)
for reaction in self.reactions {
let reactionView: ReactionButton = ReactionButton(viewModel: reaction, showNumber: showNumbers)
let reactionViewWidth: CGFloat = reactionView.systemLayoutSizeFitting(maxSize).width
reactionViews.append(reactionView)
while reactions.count > 0 && line.count < maxEmojisPerLine {
line.append(reactions.removeFirst())
// Check if we need to create a new line
let stackViewWidth: CGFloat = (lineStackView.arrangedSubviews.isEmpty ?
0 :
lineStackView.systemLayoutSizeFitting(maxSize).width
)
if stackViewWidth + reactionViewWidth > maxWidth {
lineStackView = createLineStackView()
reactionContainerView.addArrangedSubview(lineStackView)
}
updateCollapsedReactions(line)
numberOfLines += 1
lineStackView.addArrangedSubview(reactionView)
reactionView.set(.width, to: reactionViewWidth)
}
if numberOfLines > 1 {
collapseButton.isHidden = false
}
else {
showingAllReactions = false
}
}
private func prepareForUpdate() {
for subview in reactionContainerView.arrangedSubviews {
reactionContainerView.removeArrangedSubview(subview)
subview.removeFromSuperview()
}
collapseButton.isHidden = true
reactionViews = []
}
public func showAllEmojis() {
guard !showingAllReactions else { return }
showingAllReactions = true
update(reactions, showNumbers: showNumbers)
update(reactions, maxWidth: maxWidth, showingAllReactions: true, showNumbers: showNumbers)
}
public func showLessEmojis() {
guard showingAllReactions else { return }
showingAllReactions = false
update(reactions, showNumbers: showNumbers)
update(reactions, maxWidth: maxWidth, showingAllReactions: false, showNumbers: showNumbers)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

@ -477,6 +477,7 @@
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";

View file

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

View file

@ -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? = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>] = []

View file

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

View file

@ -0,0 +1,19 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
public extension AnyPublisher {
func firstValue() -> Output? {
var value: Output?
_ = self
.receiveOnMain(immediately: true)
.sink(
receiveCompletion: { _ in },
receiveValue: { result in value = result }
)
return value
}
}

View file

@ -6,16 +6,16 @@ import Curve25519Kit
extension Box.KeyPair: Mocked {
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)
)
}
}

View file

@ -0,0 +1,28 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import PromiseKit
import SessionUtilitiesKit
class SynchronousStorage: Storage {
override func writeAsync<T>(updates: @escaping (Database) throws -> T) {
super.write(updates: updates)
}
override func writeAsync<T>(updates: @escaping (Database) throws -> T, completion: @escaping (Database, Swift.Result<T, Error>) throws -> Void) {
super.write { db in
do {
var result: T?
try db.inTransaction {
result = try updates(db)
return .commit
}
try? completion(db, .success(result!))
}
catch {
try? completion(db, .failure(error))
}
}
}
}