diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 11e679ee9..7c5ee60b3 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -177,6 +177,7 @@ 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; }; 7BCD116C27016062006330F1 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */; }; + 7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD477A727EC39F5004E2822 /* Atomic.swift */; }; 7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; }; 7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; }; @@ -1198,6 +1199,7 @@ 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 7BC707F127290ACB002817AD /* SessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCallManager.swift; sourceTree = "<group>"; }; 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = "<group>"; }; + 7BD477A727EC39F5004E2822 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; }; 7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = "<group>"; }; 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = "<group>"; }; 7BFD1A892745C4F000FB91B9 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = "<group>"; }; @@ -2433,6 +2435,7 @@ B8A582B0258C66C900AFD84C /* General */ = { isa = PBXGroup; children = ( + 7BD477A727EC39F5004E2822 /* Atomic.swift */, 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */, 7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */, 7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */, @@ -4796,6 +4799,7 @@ C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */, C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */, + 7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */, B8BC00C0257D90E30032E807 /* General.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 9fbb5c73e..a5fcce51c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -283,49 +283,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<Void> = 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<Void> = 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) { @@ -349,44 +346,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<Void> = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: !oldThreadShouldBeVisible, - timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting + let promise: Promise<Void> = 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() { @@ -1128,7 +1122,7 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate { extension ConversationVC { - fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> { + fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> { 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 @@ -1159,7 +1153,17 @@ extension ConversationVC { } return promise - .then { MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) } + .then { _ -> Promise<Void> in + let (promise, seal) = Promise<Void>.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 @@ -1168,9 +1172,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) @@ -1208,30 +1214,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<Void> = 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<Void> = 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/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 7eea99ca0..09a4fd257 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -144,10 +144,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) } @@ -159,14 +160,17 @@ extension AppDelegate { } } - func forceSyncConfigurationNowIfNeeded(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Void> { - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { - return Promise.value(()) - } - + func forceSyncConfigurationNowIfNeeded() -> Promise<Void> { let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) let (promise, seal) = Promise<Void>.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/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index a5a3aef29..7577b37fb 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -135,7 +135,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() } @@ -147,7 +147,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+Shared.swift b/SessionMessagingKit/Database/Storage+Shared.swift index 201a34f6f..d8f5de95d 100644 --- a/SessionMessagingKit/Database/Storage+Shared.swift +++ b/SessionMessagingKit/Database/Storage+Shared.swift @@ -40,7 +40,7 @@ extension Storage { } public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? { - guard let userPublicKey = getUserPublicKey() else { return nil } + let userPublicKey = getUserHexEncodedPublicKey() var result: Contact? if let transaction = transaction { diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift index 641a055ae..da328bd28 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift @@ -2,7 +2,7 @@ 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(using: transaction) else { return nil } @@ -13,94 +13,84 @@ extension ConfigurationMessage { var openGroups: Set<String> = [] var contacts: Set<ConfigurationMessage.Contact> = [] - 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, 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) - ) - 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)") - } - - 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 bd6cef3d2..6113c5b68 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -869,12 +869,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/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<String?> = 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/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<Value> +/// The `Atomic<Value>` 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<Value> { + 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<Value> { + 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 + } +}