diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f4998ffdb..ea715c12b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -787,6 +787,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 */ @@ -1829,6 +1830,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 */ @@ -2339,6 +2341,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 */, @@ -4680,6 +4683,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 7e3aec685..0b26218e7 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -263,49 +263,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) { @@ -329,44 +326,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() { @@ -1103,7 +1097,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 @@ -1134,7 +1128,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 @@ -1143,9 +1147,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) + } // Hide the 'messageRequestView' since the request has been approved and force a config // sync to propagate the contact approval state (both must run on the main thread) @@ -1183,30 +1189,27 @@ extension ConversationVC { // Send a sync message with the details of the contact if let appDelegate = UIApplication.shared.delegate as? AppDelegate { - appDelegate.forceSyncConfigurationNowIfNeeded(with: transaction).retainUntilComplete() + appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() } } } } @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() { 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/Home/HomeVC.swift b/Session/Home/HomeVC.swift index da9153682..8bbc7a523 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 diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 30525183d..6f1182807 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -7,10 +7,11 @@ extension AppDelegate { guard Storage.shared.getUser()?.name != nil else { return } let userDefaults = UserDefaults.standard let lastSync = userDefaults[.lastConfigurationSync] ?? .distantPast - guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60, - let configurationMessage = ConfigurationMessage.getCurrent() else { return } // Sync every 2 days + guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60 else { return } // Sync every 2 days let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) - Storage.shared.write { transaction in + Storage.write { transaction in + guard let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { return } + let job = MessageSendJob(message: configurationMessage, destination: destination) JobQueue.shared.add(job, using: transaction) } @@ -22,14 +23,17 @@ extension AppDelegate { } } - func forceSyncConfigurationNowIfNeeded(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise { - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { - return Promise.value(()) - } - + func forceSyncConfigurationNowIfNeeded() -> Promise { let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) let (promise, seal) = Promise.pending() + + // 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 + } + MessageSender.send(configurationMessage, to: destination, using: transaction).done { seal.fulfill(()) }.catch { _ in 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 e3110969d..dba878a22 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -128,7 +128,7 @@ final class NukeDataModal : Modal { appDelegate.forceSyncConfigurationNowIfNeeded().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() } @@ -140,7 +140,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 d08ef570e..da328bd28 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,91 +13,84 @@ 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 - SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID) - ) + 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) + + case .openGroup: + if let threadId: String = thread.uniqueId, let v2OpenGroup = storage.getV2OpenGroup(for: threadId) { + openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)") + } - return ConfigurationMessage.Contact( - publicKey: contact.sessionID, - displayName: (contact.name ?? contact.sessionID), - profilePictureURL: profilePictureURL, - profileKey: profileKey, - hasIsApproved: true, - isApproved: contact.isApproved, - hasIsBlocked: true, - isBlocked: SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID), - hasDidApproveMe: true, - didApproveMe: contact.didApproveMe + 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 + SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID) ) + 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: SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID), + 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 ab23a33f0..aa00ecba8 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -827,12 +827,14 @@ extension MessageReceiver { // a new configuration message (otherwise the `contact` will be loaded direct from the database and the // `didApproveMe` value won't have been updated) DispatchQueue.global(qos: .background).async { - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent() else { - return + Storage.write { transaction in + guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { + return + } + + let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey) + MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete() } - - let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey) - MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete() } } 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 + } +}