diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f5828d186..9ae2c1328 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -778,6 +778,7 @@ FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; }; FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; }; FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; + FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645727EC1F4000808CA1 /* Atomic.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1811,6 +1812,7 @@ FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; + FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2321,6 +2323,7 @@ C33FDB8A255A581200E217F9 /* AppContext.h */, C33FDB85255A581100E217F9 /* AppContext.m */, C3C2A5D12553860800C340D1 /* Array+Utilities.swift */, + FDFD645727EC1F4000808CA1 /* Atomic.swift */, C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */, B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */, @@ -4629,6 +4632,7 @@ C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */, C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */, + FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */, C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */, B8BC00C0257D90E30032E807 /* General.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 661584fab..e8d77f76e 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -48,14 +48,17 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc self.blockedBanner.alpha = 0 }, completion: { _ in if let contact: Contact = Storage.shared.getContact(with: publicKey) { - Storage.shared.write { transaction in - guard let transaction = transaction as? YapDatabaseReadWriteTransaction else { return } - - contact.isBlocked = false - Storage.shared.setContact(contact, using: transaction) - - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() - } + Storage.shared.write( + with: { transaction in + guard let transaction = transaction as? YapDatabaseReadWriteTransaction else { return } + + contact.isBlocked = false + Storage.shared.setContact(contact, using: transaction) + }, + completion: { + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() + } + ) } }) } @@ -264,49 +267,46 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let linkPreviewDraft = snInputView.linkPreviewInfo?.draft let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) - Storage.write(with: { transaction in - let promise: Promise = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: !oldThreadShouldBeVisible, - timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting - ) - .map { [weak self] _ in - self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage) - - Storage.write(with: { transaction in - message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction) - }, completion: { [weak self] in - tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview) - - Storage.shared.write( - with: { transaction in - tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction) - }, - completion: { [weak self] in - // At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing - // the height of the new message cell - self?.scrollToBottom(isAnimated: false) - } - ) - - Storage.shared.write { transaction in - MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction) + let promise: Promise = self.approveMessageRequestIfNeeded( + for: self.thread, + isNewThread: !oldThreadShouldBeVisible, + timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting + ) + .map { [weak self] _ in + self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage) + + Storage.write(with: { transaction in + message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction) + }, completion: { [weak self] in + tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview) + + Storage.shared.write( + with: { transaction in + tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction) + }, + completion: { [weak self] in + // At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing + // the height of the new message cell + self?.scrollToBottom(isAnimated: false) } - - self?.handleMessageSent() - }) - } - - // Show an error indicating that approving the thread failed - promise.catch(on: DispatchQueue.main) { [weak self] _ in - let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) - self?.present(alert, animated: true, completion: nil) - } - - promise.retainUntilComplete() - }) + ) + + Storage.shared.write { transaction in + MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction) + } + + self?.handleMessageSent() + }) + } + + // Show an error indicating that approving the thread failed + promise.catch(on: DispatchQueue.main) { [weak self] _ in + let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self?.present(alert, animated: true, completion: nil) + } + + promise.retainUntilComplete() } func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) { @@ -330,44 +330,41 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let oldThreadShouldBeVisible: Bool = thread.shouldBeVisible let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) - Storage.write(with: { transaction in - let promise: Promise = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: !oldThreadShouldBeVisible, - timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting + let promise: Promise = self.approveMessageRequestIfNeeded( + for: self.thread, + isNewThread: !oldThreadShouldBeVisible, + timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting + ) + .map { [weak self] _ in + Storage.write( + with: { transaction in + tsMessage.save(with: transaction) + // The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet + }, + completion: { [weak self] in + Storage.write(with: { transaction in + MessageSender.send(message, with: attachments, in: thread, using: transaction) + }, completion: { [weak self] in + // At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing + // the height of the new message cell + self?.scrollToBottom(isAnimated: false) + }) + self?.handleMessageSent() + + // Attachment successfully sent - dismiss the screen + onComplete?() + } ) - .map { [weak self] _ in - Storage.write( - with: { transaction in - tsMessage.save(with: transaction) - // The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet - }, - completion: { [weak self] in - Storage.write(with: { transaction in - MessageSender.send(message, with: attachments, in: thread, using: transaction) - }, completion: { [weak self] in - // At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing - // the height of the new message cell - self?.scrollToBottom(isAnimated: false) - }) - self?.handleMessageSent() - - // Attachment successfully sent - dismiss the screen - onComplete?() - } - ) - } + } + + // Show an error indicating that approving the thread failed + promise.catch(on: DispatchQueue.main) { [weak self] _ in + let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self?.present(alert, animated: true, completion: nil) + } - // Show an error indicating that approving the thread failed - promise.catch(on: DispatchQueue.main) { [weak self] _ in - let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) - self?.present(alert, animated: true, completion: nil) - } - - promise.retainUntilComplete() - }) + promise.retainUntilComplete() } func handleMessageSent() { @@ -1104,7 +1101,7 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate { extension ConversationVC { - fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise { + fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, isNewThread: Bool, timestamp: UInt64) -> Promise { guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) } // If the contact doesn't exist then we should create it so we can store the 'isApproved' state @@ -1135,7 +1132,17 @@ extension ConversationVC { } return promise - .then { MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) } + .then { _ -> Promise in + let (promise, seal) = Promise.pending() + Storage.writeSync { transaction in + MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) + .done { seal.fulfill(()) } + .catch { _ in seal.fulfill(()) } // Fulfill even if this failed; the configuration in the swarm should be at most 2 days old + .retainUntilComplete() + } + + return promise + } .map { _ in if self?.presentedViewController is ModalActivityIndicatorViewController { self?.dismiss(animated: true, completion: nil) // Dismiss the loader @@ -1144,9 +1151,11 @@ extension ConversationVC { } .map { _ in // Default 'didApproveMe' to true for the person approving the message request - contact.isApproved = true - contact.didApproveMe = (contact.didApproveMe || !isNewThread) - Storage.shared.setContact(contact, using: transaction) + Storage.write { transaction in + contact.isApproved = true + contact.didApproveMe = (contact.didApproveMe || !isNewThread) + Storage.shared.setContact(contact, using: transaction) + } // Send a sync message with the details of the contact MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() @@ -1189,23 +1198,20 @@ extension ConversationVC { } @objc func acceptMessageRequest() { - Storage.write { transaction in - let promise: Promise = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: false, - timestamp: NSDate.millisecondTimestamp() - ) - - // Show an error indicating that approving the thread failed - promise.catch(on: DispatchQueue.main) { [weak self] _ in - let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) - self?.present(alert, animated: true, completion: nil) - } - - promise.retainUntilComplete() + let promise: Promise = self.approveMessageRequestIfNeeded( + for: self.thread, + isNewThread: false, + timestamp: NSDate.millisecondTimestamp() + ) + + // Show an error indicating that approving the thread failed + promise.catch(on: DispatchQueue.main) { [weak self] _ in + let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) + self?.present(alert, animated: true, completion: nil) } + + promise.retainUntilComplete() } @objc func deleteMessageRequest() { @@ -1244,11 +1250,11 @@ extension ConversationVC { // Delete all thread content self?.thread.removeAllThreadInteractions(with: transaction) self?.thread.remove(with: transaction) - - // Force a config sync and pop to the previous screen (both must run on the main thread) - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() }, completion: { [weak self] in + // Force a config sync and pop to the previous screen + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() + DispatchQueue.main.async { self?.navigationController?.popViewController(animated: true) } diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift index a2fe25fc5..2132d417b 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift @@ -9,7 +9,7 @@ final class LinkPreviewView : UIView { private lazy var imageViewContainerHeightConstraint = imageView.set(.height, to: 100) private lazy var sentLinkPreviewTextColor: UIColor = { - let isOutgoing = (viewItem!.interaction.interactionType() == .outgoingMessage) + let isOutgoing = (viewItem?.interaction.interactionType() == .outgoingMessage) switch (isOutgoing, AppModeManager.shared.currentAppMode) { case (true, .dark), (false, .light): return .black case (true, .light): return Colors.grey diff --git a/Session/Conversations/Views & Modals/BlockedModal.swift b/Session/Conversations/Views & Modals/BlockedModal.swift index 11135e6f6..de6a87f3b 100644 --- a/Session/Conversations/Views & Modals/BlockedModal.swift +++ b/Session/Conversations/Views & Modals/BlockedModal.swift @@ -66,16 +66,19 @@ final class BlockedModal: Modal { @objc private func unblock() { let publicKey: String = self.publicKey - Storage.shared.write { transaction in - guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else { - return + Storage.shared.write( + with: { transaction in + guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else { + return + } + + contact.isBlocked = false + Storage.shared.setContact(contact, using: transaction as Any) + }, + completion: { + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() } - - contact.isBlocked = false - Storage.shared.setContact(contact, using: transaction as Any) - - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() - } + ) presentingViewController?.dismiss(animated: true, completion: nil) } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index f401d0bbe..b5b694287 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -84,6 +84,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv // MARK: Lifecycle override func viewDidLoad() { super.viewDidLoad() + + // Note: This is a hack to ensure `isRTL` is initially gets run on the main thread so the value is cached (it gets + // called on background threads and if it hasn't cached the value then it can cause odd performance issues since + // it accesses UIKit) + _ = CurrentAppContext().isRTL + // Threads (part 1) dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to) // Preparation @@ -517,10 +523,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv contact.isBlocked = true Storage.shared.setContact(contact, using: transaction as Any) - - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() }, completion: { + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() + DispatchQueue.main.async { tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade) } @@ -537,10 +543,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv contact.isBlocked = false Storage.shared.setContact(contact, using: transaction as Any) - - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() }, completion: { + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() + DispatchQueue.main.async { tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade) } diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift index 5d4a21387..0d3e61a13 100644 --- a/Session/Home/Message Requests/MessageRequestsViewController.swift +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -336,35 +336,38 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE", comment: ""), message: nil, preferredStyle: .actionSheet) alertVC.addAction(UIAlertAction(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON", comment: ""), style: .destructive) { _ in // Clear the requests - Storage.write { [weak self] transaction in - threads.forEach { thread in - if let uniqueId: String = thread.uniqueId { - Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction) - } - - self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in - if threadNeedsSync { + Storage.write( + with: { [weak self] transaction in + threads.forEach { thread in + if let uniqueId: String = thread.uniqueId { + Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction) + } + + self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in + if threadNeedsSync { + needsSync = true + } + } + + // Block the contact + if + let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), + !thread.isBlocked(), + let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction) + { + contact.isBlocked = true + Storage.shared.setContact(contact, using: transaction) needsSync = true } } - - // Block the contact - if - let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), - !thread.isBlocked(), - let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction) - { - contact.isBlocked = true - Storage.shared.setContact(contact, using: transaction) - needsSync = true + }, + completion: { + // Force a config sync + if needsSync { + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() } } - - // Force a config sync - if needsSync { - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() - } - } + ) }) alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil)) self.present(alertVC, animated: true, completion: nil) @@ -375,23 +378,26 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet) alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in - Storage.write { [weak self] transaction in - Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction) - self?.updateContactAndThread(thread: thread, with: transaction) - - // Block the contact - if - let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), - !thread.isBlocked(), - let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction) - { - contact.isBlocked = true - Storage.shared.setContact(contact, using: transaction) + Storage.write( + with: { [weak self] transaction in + Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction) + self?.updateContactAndThread(thread: thread, with: transaction) + + // Block the contact + if + let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), + !thread.isBlocked(), + let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction) + { + contact.isBlocked = true + Storage.shared.setContact(contact, using: transaction) + } + }, + completion: { + // Force a config sync + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() } - - // Force a config sync - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() - } + ) }) alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil)) self.present(alertVC, animated: true, completion: nil) diff --git a/Session/Meta/MainAppContext.m b/Session/Meta/MainAppContext.m index 66948daf6..2fad58d41 100644 --- a/Session/Meta/MainAppContext.m +++ b/Session/Meta/MainAppContext.m @@ -166,6 +166,7 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic - (BOOL)isRTL { + // FIXME: We should try to remove this as we've had to add a hack to ensure the first call to this runs on the main thread static BOOL isRTL = NO; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index a9361c132..c3b3ef87c 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -127,7 +127,7 @@ final class NukeDataModal : Modal { MessageSender.syncConfiguration(forceSyncNow: true).ensure(on: DispatchQueue.main) { self?.dismiss(animated: true, completion: nil) // Dismiss the loader UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later - General.Cache.cachedEncodedPublicKey = nil // Remove the cached key so it gets re-cached on next access + General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access NotificationCenter.default.post(name: .dataNukeRequested, object: nil) }.retainUntilComplete() } @@ -139,7 +139,7 @@ final class NukeDataModal : Modal { self?.dismiss(animated: true, completion: nil) // Dismiss the loader let potentiallyMaliciousSnodes = confirmations.compactMap { $0.value == false ? $0.key : nil } if potentiallyMaliciousSnodes.isEmpty { - General.Cache.cachedEncodedPublicKey = nil // Remove the cached key so it gets re-cached on next access + General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later NotificationCenter.default.post(name: .dataNukeRequested, object: nil) } else { diff --git a/SessionMessagingKit/Database/Storage+ClosedGroups.swift b/SessionMessagingKit/Database/Storage+ClosedGroups.swift index 05e9fc1a5..a21d6d551 100644 --- a/SessionMessagingKit/Database/Storage+ClosedGroups.swift +++ b/SessionMessagingKit/Database/Storage+ClosedGroups.swift @@ -10,13 +10,19 @@ extension Storage { private static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection" public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [ECKeyPair] { + var result: [ECKeyPair] = [] + Storage.read { transaction in + result = self.getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction) + } + return result + } + + public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> [ECKeyPair] { let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey) var timestampsAndKeyPairs: [(timestamp: Double, keyPair: ECKeyPair)] = [] - Storage.read { transaction in - transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in - guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return } - timestampsAndKeyPairs.append((timestamp, keyPair)) - } + transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in + guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return } + timestampsAndKeyPairs.append((timestamp, keyPair)) } return timestampsAndKeyPairs.sorted { $0.timestamp < $1.timestamp }.map { $0.keyPair } } @@ -24,6 +30,10 @@ extension Storage { public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> ECKeyPair? { return getClosedGroupEncryptionKeyPairs(for: groupPublicKey).last } + + public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> ECKeyPair? { + return getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction).last + } public func addClosedGroupEncryptionKeyPair(_ keyPair: ECKeyPair, for groupPublicKey: String, using transaction: Any) { let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey) @@ -39,10 +49,14 @@ extension Storage { public func getUserClosedGroupPublicKeys() -> Set { var result: Set = [] Storage.read { transaction in - result = Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection)) + result = self.getUserClosedGroupPublicKeys(using: transaction) } return result } + + public func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set { + return Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection)) + } public func addClosedGroupPublicKey(_ groupPublicKey: String, using transaction: Any) { (transaction as! YapDatabaseReadWriteTransaction).setObject(groupPublicKey, forKey: groupPublicKey, inCollection: Storage.closedGroupPublicKeyCollection) @@ -81,4 +95,8 @@ extension Storage { public func isClosedGroup(_ publicKey: String) -> Bool { getUserClosedGroupPublicKeys().contains(publicKey) } + + public func isClosedGroup(_ publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool { + getUserClosedGroupPublicKeys(using: transaction).contains(publicKey) + } } diff --git a/SessionMessagingKit/Database/Storage+Shared.swift b/SessionMessagingKit/Database/Storage+Shared.swift index b2752d29d..d8f5de95d 100644 --- a/SessionMessagingKit/Database/Storage+Shared.swift +++ b/SessionMessagingKit/Database/Storage+Shared.swift @@ -36,11 +36,21 @@ extension Storage { } @objc public func getUser() -> Contact? { - guard let userPublicKey = getUserPublicKey() else { return nil } + return getUser(using: nil) + } + + public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? { + let userPublicKey = getUserHexEncodedPublicKey() var result: Contact? - Storage.read { transaction in + + if let transaction = transaction { result = Storage.shared.getContact(with: userPublicKey, using: transaction) } + else { + Storage.read { transaction in + result = Storage.shared.getContact(with: userPublicKey, using: transaction) + } + } return result } } diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift index 637f8f046..d227ec9e4 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift @@ -2,9 +2,9 @@ import SessionUtilitiesKit extension ConfigurationMessage { - public static func getCurrent(with transaction: YapDatabaseReadWriteTransaction? = nil) -> ConfigurationMessage? { + public static func getCurrent(with transaction: YapDatabaseReadTransaction) -> ConfigurationMessage? { let storage = Storage.shared - guard let user = storage.getUser() else { return nil } + guard let user = storage.getUser(using: transaction) else { return nil } let displayName = user.name let profilePictureURL = user.profilePictureURL @@ -13,92 +13,85 @@ extension ConfigurationMessage { var openGroups: Set = [] var contacts: Set = [] - let populateDataClosure: (YapDatabaseReadTransaction) -> () = { transaction in - TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in - guard let thread = object as? TSGroupThread else { return } - - switch thread.groupModel.groupType { - case .closedGroup: - guard thread.isCurrentUserMemberInGroup() else { return } - - let groupID = thread.groupModel.groupId - let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID) - - guard storage.isClosedGroup(groupPublicKey), let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { - return - } - - let closedGroup = ClosedGroup( - publicKey: groupPublicKey, - name: thread.groupModel.groupName!, - encryptionKeyPair: encryptionKeyPair, - members: Set(thread.groupModel.groupMemberIds), - admins: Set(thread.groupModel.groupAdminIds), - expirationTimer: thread.disappearingMessagesDuration(with: transaction) - ) - closedGroups.insert(closedGroup) - - case .openGroup: - if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) { - openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)") - } - - default: break - } - } - - let currentUserPublicKey: String = getUserHexEncodedPublicKey() + TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in + guard let thread = object as? TSGroupThread else { return } - contacts = storage.getAllContacts(with: transaction) - .filter { contact -> Bool in - let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID) + switch thread.groupModel.groupType { + case .closedGroup: + guard thread.isCurrentUserMemberInGroup() else { return } + + let groupID = thread.groupModel.groupId + let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID) - return ( - // Skip the current user - contact.sessionID != currentUserPublicKey && - // Contacts which have visible threads - TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && ( - - // Include already approved contacts - contact.isApproved || - contact.didApproveMe || - - // Sync blocked contacts - contact.isBlocked || - contact.hasBeenBlocked - ) + guard + storage.isClosedGroup(groupPublicKey, using: transaction), + let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction) + else { + return + } + + let closedGroup = ClosedGroup( + publicKey: groupPublicKey, + name: (thread.groupModel.groupName ?? ""), + encryptionKeyPair: encryptionKeyPair, + members: Set(thread.groupModel.groupMemberIds), + admins: Set(thread.groupModel.groupAdminIds), + expirationTimer: thread.disappearingMessagesDuration(with: transaction) ) - } - .map { contact -> ConfigurationMessage.Contact in - // Can just default the 'hasX' values to true as they will be set to this - // when converting to proto anyway - let profilePictureURL = contact.profilePictureURL - let profileKey = contact.profileEncryptionKey?.keyData + closedGroups.insert(closedGroup) - return ConfigurationMessage.Contact( - publicKey: contact.sessionID, - displayName: (contact.name ?? contact.sessionID), - profilePictureURL: profilePictureURL, - profileKey: profileKey, - hasIsApproved: true, - isApproved: contact.isApproved, - hasIsBlocked: true, - isBlocked: contact.isBlocked, - hasDidApproveMe: true, - didApproveMe: contact.didApproveMe + case .openGroup: + if let threadId: String = thread.uniqueId, let v2OpenGroup = storage.getV2OpenGroup(for: threadId) { + openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)") + } + + default: break + } + } + + let currentUserPublicKey: String = getUserHexEncodedPublicKey() + + contacts = storage.getAllContacts(with: transaction) + .compactMap { contact -> ConfigurationMessage.Contact? in + let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID) + + guard + // Skip the current user + contact.sessionID != currentUserPublicKey && + // Contacts which have visible threads + TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && ( + + // Include already approved contacts + contact.isApproved || + contact.didApproveMe || + + // Sync blocked contacts + contact.isBlocked || + contact.hasBeenBlocked ) + else { + return nil } - .asSet() + + // Can just default the 'hasX' values to true as they will be set to this + // when converting to proto anyway + let profilePictureURL = contact.profilePictureURL + let profileKey = contact.profileEncryptionKey?.keyData + + return ConfigurationMessage.Contact( + publicKey: contact.sessionID, + displayName: (contact.name ?? contact.sessionID), + profilePictureURL: profilePictureURL, + profileKey: profileKey, + hasIsApproved: true, + isApproved: contact.isApproved, + hasIsBlocked: true, + isBlocked: contact.isBlocked, + hasDidApproveMe: true, + didApproveMe: contact.didApproveMe + ) } - - // If we are provided with a transaction then read the data based on the state of the database - // from within the transaction rather than the state in disk - if let transaction: YapDatabaseReadWriteTransaction = transaction { - populateDataClosure(transaction) - } - else { - Storage.read { transaction in populateDataClosure(transaction) } - } + .asSet() return ConfigurationMessage( displayName: displayName, diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 2274f5573..9f414a048 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -803,7 +803,7 @@ extension MessageReceiver { // Force a config sync to ensure all devices know the contact approval state if desired guard forceConfigSync else { return } - MessageSender.syncConfiguration(forceSyncNow: true, with: transaction).retainUntilComplete() + MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() } public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) { diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 127cce708..3165d5202 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -17,15 +17,18 @@ public protocol SessionMessagingKitStorageProtocol { func getUserKeyPair() -> ECKeyPair? func getUserED25519KeyPair() -> Box.KeyPair? func getUser() -> Contact? + func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? func getAllContacts() -> Set func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set // MARK: - Closed Groups func getUserClosedGroupPublicKeys() -> Set + func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set func getZombieMembers(for groupPublicKey: String) -> Set func setZombieMembers(for groupPublicKey: String, to zombies: Set, using transaction: Any) func isClosedGroup(_ publicKey: String) -> Bool + func isClosedGroup(_ publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool // MARK: - Jobs diff --git a/SessionMessagingKit/Utilities/General.swift b/SessionMessagingKit/Utilities/General.swift index 565338f65..d2e4e0c96 100644 --- a/SessionMessagingKit/Utilities/General.swift +++ b/SessionMessagingKit/Utilities/General.swift @@ -2,7 +2,7 @@ import Foundation public enum General { public enum Cache { - public static var cachedEncodedPublicKey: String? = nil + public static var cachedEncodedPublicKey: Atomic = Atomic(nil) } } @@ -14,10 +14,10 @@ public class GeneralUtilities: NSObject { } public func getUserHexEncodedPublicKey() -> String { - if let cachedKey: String = General.Cache.cachedEncodedPublicKey { return cachedKey } + if let cachedKey: String = General.Cache.cachedEncodedPublicKey.wrappedValue { return cachedKey } if let keyPair = OWSIdentityManager.shared().identityKeyPair() { // Can be nil under some circumstances - General.Cache.cachedEncodedPublicKey = keyPair.hexEncodedPublicKey + General.Cache.cachedEncodedPublicKey.mutate { $0 = keyPair.hexEncodedPublicKey } return keyPair.hexEncodedPublicKey } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 0e0ff5db3..91a193a7e 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -162,7 +162,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView message.sentTimestamp = NSDate.millisecondTimestamp() message.text = (isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ? ( - (messageText?.isEmpty == true ? + (messageText?.isEmpty == true || (attachments[0].text() == messageText) ? attachments[0].text() : "\(attachments[0].text() ?? "")\n\n\(messageText ?? "")" ) diff --git a/SessionUtilitiesKit/General/Atomic.swift b/SessionUtilitiesKit/General/Atomic.swift new file mode 100644 index 000000000..14baeed68 --- /dev/null +++ b/SessionUtilitiesKit/General/Atomic.swift @@ -0,0 +1,44 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +import Foundation + +// MARK: - Atomic +/// The `Atomic` wrapper is a generic wrapper providing a thread-safe way to get and set a value +/// +/// A write-up on the need for this class and it's approach can be found here: +/// https://www.vadimbulavin.com/swift-atomic-properties-with-property-wrappers/ +/// there is also another approach which can be taken but it requires separate types for collections and results in +/// a somewhat inconsistent interface between different `Atomic` wrappers +@propertyWrapper +public class Atomic { + private let queue: DispatchQueue = DispatchQueue(label: "io.oxen.\(UUID().uuidString)") + private var value: Value + + /// In order to change the value you **must** use the `mutate` function + public var wrappedValue: Value { + return queue.sync { return value } + } + + /// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections + public var projectedValue: Atomic { + return self + } + + // MARK: - Initialization + public init(_ initialValue: Value) { + self.value = initialValue + } + + // MARK: - Functions + + public func mutate(_ mutation: (inout Value) -> Void) { + return queue.sync { + mutation(&value) + } + } +} + +extension Atomic where Value: CustomDebugStringConvertible { + var debugDescription: String { + return value.debugDescription + } +} diff --git a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift index d4cd9db69..50f678b5c 100644 --- a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift +++ b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift @@ -1,4 +1,5 @@ import PromiseKit +import SessionUtilitiesKit extension MessageSender { @@ -105,14 +106,16 @@ extension MessageSender { return promise } - public static func syncConfiguration(forceSyncNow: Bool = true, with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise { - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { - return Promise.value(()) - } - + public static func syncConfiguration(forceSyncNow: Bool = true) -> Promise { let (promise, seal) = Promise.pending() - let sendMessage: (YapDatabaseReadTransaction) -> () = { transaction in - let destination: Message.Destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) + let destination: Message.Destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) + + // Note: SQLite only supports a single write thread so we can be sure this will retrieve the most up-to-date data + Storage.writeSync { transaction in + guard Storage.shared.getUser(using: transaction)?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { + seal.fulfill(()) + return + } if forceSyncNow { MessageSender.send(configurationMessage, to: destination, using: transaction).done { @@ -128,15 +131,6 @@ extension MessageSender { } } - // If we are provided with a transaction then read the data based on the state of the database - // from within the transaction rather than the state in disk - if let transaction: YapDatabaseReadWriteTransaction = transaction { - sendMessage(transaction) - } - else { - Storage.writeSync { transaction in sendMessage(transaction) } - } - return promise } } @@ -144,6 +138,6 @@ extension MessageSender { extension MessageSender { @objc(forceSyncConfigurationNow) public static func objc_forceSyncConfigurationNow() { - return syncConfiguration(forceSyncNow: true, with: nil).retainUntilComplete() + return syncConfiguration(forceSyncNow: true).retainUntilComplete() } }