diff --git a/Podfile b/Podfile index a6b96cb29..c903f9d7d 100644 --- a/Podfile +++ b/Podfile @@ -24,8 +24,7 @@ abstract_target 'GlobalDependencies' do # Dependencies to be included only in all extensions/frameworks abstract_target 'FrameworkAndExtensionDependencies' do - # TODO: Swap this to use an oxen-io fork - pod 'Curve25519Kit', git: 'https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git', branch: 'session' + pod 'Curve25519Kit', git: 'https://github.com/oxen-io/session-ios-curve-25519-kit.git', branch: 'session-version' pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version' target 'SessionNotificationServiceExtension' diff --git a/Podfile.lock b/Podfile.lock index 17abfa0e7..f504ecaa4 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -123,7 +123,7 @@ PODS: DEPENDENCIES: - AFNetworking - CryptoSwift - - Curve25519Kit (from `https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git`, branch `session`) + - Curve25519Kit (from `https://github.com/oxen-io/session-ios-curve-25519-kit.git`, branch `session-version`) - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) - Nimble - NVActivityIndicatorView @@ -156,8 +156,8 @@ SPEC REPOS: EXTERNAL SOURCES: Curve25519Kit: - :branch: session - :git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git + :branch: session-version + :git: https://github.com/oxen-io/session-ios-curve-25519-kit.git Mantle: :branch: signal-master :git: https://github.com/signalapp/Mantle @@ -175,8 +175,8 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: Curve25519Kit: - :commit: a23049232dc6c18928cdacfbcef287dad954c5c6 - :git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git + :commit: b79c2ace600bfd3784e9c33cf1f254b121312edc + :git: https://github.com/oxen-io/session-ios-curve-25519-kit.git Mantle: :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 :git: https://github.com/signalapp/Mantle @@ -214,6 +214,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 918ef11baf24eac2df681cd6a3781f536f9d384a +PODFILE CHECKSUM: 2cc64d50f25c3b1627c3e958ae50e25fead25564 COCOAPODS: 1.11.2 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index fa898d92a..64c422af3 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -771,6 +771,8 @@ F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; + FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; }; + FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */; }; FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; }; FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; }; FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; }; @@ -1906,6 +1908,8 @@ F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = ""; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = ""; }; + FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSV4Migration.swift; sourceTree = ""; }; FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; @@ -2564,6 +2568,7 @@ isa = PBXGroup; children = ( B8B32020258B1A650020074B /* Contact.swift */, + FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */, ); path = Contacts; sourceTree = ""; @@ -3237,6 +3242,7 @@ children = ( B8B32044258C117C0020074B /* ContactsMigration.swift */, FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */, + FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */, C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */, C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */, C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */, @@ -4988,6 +4994,7 @@ C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */, C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */, C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */, + FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */, FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */, C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */, C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */, @@ -5117,6 +5124,7 @@ C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */, FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */, + FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */, FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index fbd7696a0..3aa4e499d 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2,6 +2,7 @@ import UIKit import CoreServices import Photos import PhotosUI +import Sodium import SessionUtilitiesKit import SignalUtilitiesKit @@ -813,6 +814,83 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc userDetailsSheet.modalTransitionStyle = .crossDissolve present(userDetailsSheet, animated: true, completion: nil) } + + func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String) { + // If the sessionId is blinded then check if there is an existing un-blinded thread with the contact + if SessionId.Prefix(from: sessionId) == .blinded { + // TODO: Ensure the above case isn't going to be an issue due to legacy messages? + // Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard + // sessionId, as a result in order to see if there is an unblinded contact for this blindedId we + // can only really generate blinded ids for each contact and check if any match + // + // Due to this we have made a few optimisations to try and early-out as often as possible, first + // we try to retrieve a direct cached mapping + if let mapping: BlindedIdMapping = Storage.shared.getBlindedIdMapping(with: sessionId) { + let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId) + let conversationVC: ConversationVC = ConversationVC(thread: thread) + + self.navigationController?.pushViewController(conversationVC, animated: true) + return + } + + var didFindContact: Bool = false + + // Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match + ContactUtilities.enumerateApprovedContactThreads { contactThread, contact, stop in + guard Sodium().sessionId(contact.sessionID, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else { + return + } + + // Cache the mapping + let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: contact.sessionID, serverPublicKey: openGroupPublicKey) + Storage.shared.cacheBlindedIdMapping(mapping) + + // Open the existing thread + let conversationVC: ConversationVC = ConversationVC(thread: contactThread) + self.navigationController?.pushViewController(conversationVC, animated: true) + + didFindContact = true + stop.pointee = true + } + + // Don't continue if we found the contact + guard !didFindContact else { return } + + // Lastly loop through existing id mappings (in case the user is looking at a different SOGS but once had + // a thread with this contact in a different SOGS and had cached the mapping) + Storage.shared.enumerateBlindedIdMapping { mapping, stop in + guard mapping.serverPublicKey != openGroupPublicKey else { return } + guard Sodium().sessionId(mapping.sessionId, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else { + return + } + + // Cache the new mapping + let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId) + let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: mapping.sessionId, serverPublicKey: openGroupPublicKey) + Storage.shared.cacheBlindedIdMapping(newMapping) + + // Open the existing thread + let conversationVC: ConversationVC = ConversationVC(thread: thread) + self.navigationController?.pushViewController(conversationVC, animated: true) + + didFindContact = true + stop.pointee = true + } + + // Don't continue if we found the contact + guard !didFindContact else { return } + } + + // Just create a new thread with the provided sessionId + let thread = TSContactThread.getOrCreateThread( + contactSessionID: sessionId, + openGroupServer: openGroupServer, + openGroupPublicKey: openGroupPublicKey + ) + let conversationVC: ConversationVC = ConversationVC(thread: thread) + + self.navigationController?.pushViewController(conversationVC, animated: true) + } // MARK: Voice Message Playback @objc func handleAudioDidFinishPlayingNotification(_ notification: Notification) { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 0e2159de4..d8d8dc079 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1,3 +1,4 @@ +import UIKit import SessionUIKit import SessionMessagingKit @@ -13,9 +14,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat let focusedMessageID: String? // This is used for global search var focusedMessageIndexPath: IndexPath? var unreadViewItems: [ConversationViewItem] = [] - var scrollButtonBottomConstraint: NSLayoutConstraint? - var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint? - var messageRequestsViewBotomConstraint: NSLayoutConstraint? + var isReplacingThread: Bool = false + // Search var isShowingSearchUI = false var lastSearchedText: String? @@ -40,7 +40,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat var audioSession: OWSAudioSession { Environment.shared.audioSession } var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection } var viewItems: [ConversationViewItem] { viewModel.viewState.viewItems } - override var canBecomeFirstResponder: Bool { true } + + override var canBecomeFirstResponder: Bool { + // Need to return false during the swap between threads to prevent keyboard dismissal + !isReplacingThread + } override var inputAccessoryView: UIView? { if let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup && !thread.isCurrentUserMemberInGroup() { @@ -102,6 +106,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat private static let messageRequestButtonHeight: CGFloat = 34 + var scrollButtonBottomConstraint: NSLayoutConstraint? + var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint? + var messageRequestsViewBotomConstraint: NSLayoutConstraint? + lazy var titleView: ConversationTitleView = { let result = ConversationTitleView(thread: thread) result.delegate = self @@ -363,6 +371,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat notificationCenter.addObserver(self, selector: #selector(handleGroupUpdatedNotification), name: .groupThreadUpdated, object: nil) notificationCenter.addObserver(self, selector: #selector(sendScreenshotNotificationIfNeeded), name: UIApplication.userDidTakeScreenshotNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(handleMessageSentStatusChanged), name: .messageSentStatusDidChange, object: nil) + notificationCenter.addObserver(self, selector: #selector(handleContactThreadReplaced(_:)), name: .contactThreadReplaced, object: nil) // Mentions MentionsManager.populateUserPublicKeyCacheIfNeeded(for: thread.uniqueId!) // Draft @@ -428,6 +437,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + + // Don't set the draft or resign the first responder if we are replacing the thread (want the keyboard + // to appear to remain focussed) + guard !isReplacingThread else { return } + let text = snInputView.text Storage.write { transaction in self.thread.setDraft(text, transaction: transaction) @@ -693,6 +707,90 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat } } + @objc private func handleContactThreadReplaced(_ notification: Notification) { + // Ensure the current thread is one of the removed ones + guard let newThreadId: String = notification.userInfo?[NotificationUserInfoKey.threadId] as? String else { return } + guard let removedThreadIds: [String] = notification.userInfo?[NotificationUserInfoKey.removedThreadIds] as? [String] else { + return + } + guard let threadId: String = thread.uniqueId, removedThreadIds.contains(threadId) else { return } + + // Then look to swap the current ConversationVC with a replacement one with the new thread + DispatchQueue.main.async { + guard let navController: UINavigationController = self.navigationController else { return } + guard let viewControllerIndex: Int = navController.viewControllers.firstIndex(of: self) else { return } + guard let newThread: TSContactThread = TSContactThread.fetch(uniqueId: newThreadId) else { return } + + // Let the view controller know we are replacing the thread + self.isReplacingThread = true + + // Create the new ConversationVC and swap the old one out for it + let conversationVC: ConversationVC = ConversationVC(thread: newThread) + let currentlyOnThisScreen: Bool = (navController.topViewController == self) + + navController.viewControllers = [ + (viewControllerIndex == 0 ? + [] : + navController.viewControllers[0.. Bool { inputTextView.resignFirstResponder() } + + func inputTextViewBecomeFirstResponder() { + inputTextView.becomeFirstResponder() + } func handleLongPress() { // Not relevant in this case diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 2c932517b..347c36668 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -66,5 +66,5 @@ protocol MessageCellDelegate : AnyObject { func openURL(_ url: URL) func handleReplyButtonTapped(for viewItem: ConversationViewItem) func showUserDetails(for sessionID: String) - func startThread(with sessionID: String, openGroupServer: String, openGroupPublicKey: String) + func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String) } diff --git a/Session/Utilities/ContactUtilities.swift b/Session/Utilities/ContactUtilities.swift index 2ecaa2641..f48fa32e2 100644 --- a/Session/Utilities/ContactUtilities.swift +++ b/Session/Utilities/ContactUtilities.swift @@ -1,23 +1,24 @@ enum ContactUtilities { + private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? { + guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil } + guard thread.shouldBeVisible else { return nil } + guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else { + return nil + } + guard contact.didApproveMe else { return nil } + + return contact + } static func getAllContacts() -> [String] { // Collect all contacts - var result: [String] = [] + var result: [Contact] = [] Storage.read { transaction in TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in - guard - let thread: TSContactThread = object as? TSContactThread, - thread.shouldBeVisible, - Storage.shared.getContact( - with: thread.contactSessionID(), - using: transaction - )?.didApproveMe == true - else { - return - } + guard let contact: Contact = approvedContact(in: object, using: transaction) else { return } - result.append(thread.contactSessionID()) + result.append(contact) } } func getDisplayName(for publicKey: String) -> String { @@ -25,11 +26,24 @@ enum ContactUtilities { } // Remove the current user - if let index = result.firstIndex(of: getUserHexEncodedPublicKey()) { + if let index = result.firstIndex(where: { $0.sessionID == getUserHexEncodedPublicKey() }) { result.remove(at: index) } // Sort alphabetically - return result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } + return result + .map { contact -> String in (contact.displayName(for: .regular) ?? contact.sessionID) } + .sorted() + } + + static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer) -> ()) { + Storage.read { transaction in + TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in + guard let contactThread: TSContactThread = object as? TSContactThread else { return } + guard let contact: Contact = approvedContact(in: object, using: transaction) else { return } + + block(contactThread, contact, stop) + } + } } } diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index 5a559b6df..dc5e2fcab 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -189,7 +189,8 @@ enum MockDataGenerator { image: nil, groupId: groupId, groupType: .closedGroup, - adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId] + adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId], + moderatorIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId] ) let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction) thread.shouldBeVisible = true @@ -232,23 +233,49 @@ enum MockDataGenerator { let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0) let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0) + let groupDescriptionLength: Int = ((10..<50).randomElement(using: &ogThreadRandomGenerator) ?? 0) let serverName: String = (0.. BlindedIdMapping? { + var result: BlindedIdMapping? + Storage.read { transaction in + result = self.getBlindedIdMapping(with: blindedId, using: transaction) + } + return result + } + + public func getBlindedIdMapping(with blindedId: String, using transaction: YapDatabaseReadTransaction) -> BlindedIdMapping? { + return transaction.object(forKey: blindedId, inCollection: Storage.blindedIdCacheCollection) as? BlindedIdMapping + } + + public func cacheBlindedIdMapping(_ mapping: BlindedIdMapping) { + Storage.write { transaction in + self.cacheBlindedIdMapping(mapping, using: transaction) + } + } + + public func cacheBlindedIdMapping(_ mapping: BlindedIdMapping, using transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(mapping, forKey: mapping.blindedId, inCollection: Storage.blindedIdCacheCollection) + } + + public func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer) -> ()) { + Storage.read { transaction in + self.enumerateBlindedIdMapping(with: block, transaction: transaction) + } + } + + public func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer) -> (), transaction: YapDatabaseReadTransaction) { + transaction.enumerateRows(inCollection: Storage.blindedIdCacheCollection) { _, object, _, stop in + guard let mapping = object as? BlindedIdMapping else { return } + + block(mapping, stop) + } + } } diff --git a/SessionMessagingKit/Database/Storage+Messaging.swift b/SessionMessagingKit/Database/Storage+Messaging.swift index 48421aa49..482bdc39a 100644 --- a/SessionMessagingKit/Database/Storage+Messaging.swift +++ b/SessionMessagingKit/Database/Storage+Messaging.swift @@ -2,35 +2,6 @@ import PromiseKit import Sodium extension Storage { - - public func getAllMessageRequestThreads() -> [String: TSContactThread] { - var result: [String: TSContactThread] = [:] - - Storage.read { transaction in - result = self.getAllMessageRequestThreads(using: transaction) - } - - return result - } - - public func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] { - var result = [String: TSContactThread]() - - // FIXME: We might be able to optimise this further by filtering the SQL query `WHERE uniqueId LIKE '_c15' - let blindedThreadPrefix: String = TSContactThread.threadID(fromContactSessionID: SessionId.Prefix.blinded.rawValue) - - transaction.enumerateKeysAndObjects( - inCollection: TSContactThread.collection(), - using: { threadID, object, _ in - guard let contactThread = object as? TSContactThread else { return } - result[threadID] = contactThread - }, - withFilter: { key -> Bool in key.starts(with: blindedThreadPrefix) } - ) - - return result - } - /// Returns the ID of the thread. public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { let transaction = transaction as! YapDatabaseReadWriteTransaction @@ -180,5 +151,35 @@ extension Storage { let transaction = transaction as! YapDatabaseReadWriteTransaction transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection) } + + // MARK: - Message Request Handling + + public func getAllMessageRequestThreads() -> [String: TSContactThread] { + var result: [String: TSContactThread] = [:] + + Storage.read { transaction in + result = self.getAllMessageRequestThreads(using: transaction) + } + + return result + } + + public func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] { + var result = [String: TSContactThread]() + + // FIXME: We might be able to optimise this further by filtering the SQL query `WHERE uniqueId LIKE '_c15' + let blindedThreadPrefix: String = TSContactThread.threadID(fromContactSessionID: SessionId.Prefix.blinded.rawValue) + + transaction.enumerateKeysAndObjects( + inCollection: TSContactThread.collection(), + using: { threadID, object, _ in + guard let contactThread = object as? TSContactThread else { return } + result[threadID] = contactThread + }, + withFilter: { key -> Bool in key.starts(with: blindedThreadPrefix) } + ) + + return result + } } diff --git a/SessionMessagingKit/Database/Storage+OpenGroups.swift b/SessionMessagingKit/Database/Storage+OpenGroups.swift index 07c8228a0..5031bfe69 100644 --- a/SessionMessagingKit/Database/Storage+OpenGroups.swift +++ b/SessionMessagingKit/Database/Storage+OpenGroups.swift @@ -55,35 +55,9 @@ extension Storage { return result } - public func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { + public func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { (transaction as! YapDatabaseReadWriteTransaction).setObject(server, forKey: "SOGS.\(server.name)", inCollection: Storage.openGroupCollection) } - - // MARK: - Authorization - - private static let authTokenCollection = "SNAuthTokenCollection" - - public func getAuthToken(for room: String, on server: String) -> String? { - let collection = Storage.authTokenCollection - let key = "\(server).\(room)" - var result: String? = nil - Storage.read { transaction in - result = transaction.object(forKey: key, inCollection: collection) as? String - } - return result - } - - public func setAuthToken(for room: String, on server: String, to newValue: String, using transaction: Any) { - let collection = Storage.authTokenCollection - let key = "\(server).\(room)" - (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection) - } - - public func removeAuthToken(for room: String, on server: String, using transaction: Any) { - let collection = Storage.authTokenCollection - let key = "\(server).\(room)" - (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection) - } @@ -109,12 +83,12 @@ extension Storage { - // MARK: - Last Message Server ID + // MARK: - Open Group Sequence Number - public static let lastMessageServerIDCollection = "SNLastMessageServerIDCollection" + public static let openGroupSequenceNumberCollection = "SNOpenGroupSequenceNumberCollection" - public func getLastMessageServerID(for room: String, on server: String) -> Int64? { - let collection = Storage.lastMessageServerIDCollection + public func getOpenGroupSequenceNumber(for room: String, on server: String) -> Int64? { + let collection = Storage.openGroupSequenceNumberCollection let key = "\(server).\(room)" var result: Int64? = nil Storage.read { transaction in @@ -123,48 +97,41 @@ extension Storage { return result } - public func setLastMessageServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) { - let collection = Storage.lastMessageServerIDCollection + public func setOpenGroupSequenceNumber(for room: String, on server: String, to newValue: Int64, using transaction: Any) { + let collection = Storage.openGroupSequenceNumberCollection let key = "\(server).\(room)" (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection) } - public func removeLastMessageServerID(for room: String, on server: String, using transaction: Any) { - let collection = Storage.lastMessageServerIDCollection + public func removeOpenGroupSequenceNumber(for room: String, on server: String, using transaction: Any) { + let collection = Storage.openGroupSequenceNumberCollection let key = "\(server).\(room)" (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection) } + // MARK: - -- Open Group Inbox Latest Message Id + + public static let openGroupInboxLatestMessageIdCollection = "SNOpenGroupInboxLatestMessageIdCollection" - - // MARK: - Last Deletion Server ID - - public static let lastDeletionServerIDCollection = "SNLastDeletionServerIDCollection" - - public func getLastDeletionServerID(for room: String, on server: String) -> Int64? { - let collection = Storage.lastDeletionServerIDCollection - let key = "\(server).\(room)" + public func getOpenGroupInboxLatestMessageId(for server: String) -> Int64? { + let collection = Storage.openGroupInboxLatestMessageIdCollection var result: Int64? = nil Storage.read { transaction in - result = transaction.object(forKey: key, inCollection: collection) as? Int64 + result = transaction.object(forKey: server, inCollection: collection) as? Int64 } return result } - - public func setLastDeletionServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) { - let collection = Storage.lastDeletionServerIDCollection - let key = "\(server).\(room)" - (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection) + + public func setOpenGroupInboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) { + let collection = Storage.openGroupInboxLatestMessageIdCollection + (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection) } - - public func removeLastDeletionServerID(for room: String, on server: String, using transaction: Any) { - let collection = Storage.lastDeletionServerIDCollection - let key = "\(server).\(room)" - (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection) + + public func removeOpenGroupInboxLatestMessageId(for server: String, using transaction: Any) { + let collection = Storage.openGroupInboxLatestMessageIdCollection + (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection) } - - // MARK: - Metadata private static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection" diff --git a/SessionMessagingKit/Messages/Signal/TSInteraction.h b/SessionMessagingKit/Messages/Signal/TSInteraction.h index e6b77faf3..9dd99764c 100644 --- a/SessionMessagingKit/Messages/Signal/TSInteraction.h +++ b/SessionMessagingKit/Messages/Signal/TSInteraction.h @@ -79,6 +79,10 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value); - (void)updateTimestamp:(uint64_t)timestamp; +#pragma mark Message Request Thread Migration + +- (void)moveToThreadWithId:(NSString *)threadId; + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Messages/Signal/TSInteraction.m b/SessionMessagingKit/Messages/Signal/TSInteraction.m index f3522712d..0ad46b0de 100644 --- a/SessionMessagingKit/Messages/Signal/TSInteraction.m +++ b/SessionMessagingKit/Messages/Signal/TSInteraction.m @@ -269,6 +269,12 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) } +#pragma mark - Message Request Thread Migration + +- (void)moveToThreadWithId:(NSString *)threadId { + _uniqueThreadId = threadId; +} + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Open Groups/Models/Capabilities.swift b/SessionMessagingKit/Open Groups/Models/Capabilities.swift index 627183ace..3c5d7de12 100644 --- a/SessionMessagingKit/Open Groups/Models/Capabilities.swift +++ b/SessionMessagingKit/Open Groups/Models/Capabilities.swift @@ -35,6 +35,13 @@ extension OpenGroupAPI { public let capabilities: [Capability] public let missing: [Capability]? + + // MARK: - Initialization + + public init(capabilities: [Capability], missing: [Capability]? = nil) { + self.capabilities = capabilities + self.missing = missing + } } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index ab669549b..5a0bdeb1f 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -93,7 +93,6 @@ public final class OpenGroupManager: NSObject { } storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction) Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction) - let _ = OpenGroupAPI.legacyDeleteAuthToken(for: openGroup.room, on: openGroup.server) Storage.shared.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction) thread.removeAllThreadInteractions(with: transaction) @@ -119,7 +118,7 @@ public final class OpenGroupManager: NSObject { capabilities: capabilities ) - dependencies.storage.storeOpenGroupServer(updatedServer, using: transaction) + dependencies.storage.setOpenGroupServer(updatedServer, using: transaction) } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index c030c9482..1915dd063 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -1,3 +1,5 @@ +import Foundation +import Sodium import SignalCoreKit import SessionSnodeKit @@ -238,8 +240,10 @@ extension MessageReceiver { thread.remove(with: transaction) } } - else { - // Otherwise create and save the thread + else if SessionId.Prefix(from: sessionID) != .blinded { + // Otherwise create and save the thread (if the contact isn't a blinded contact - we don't want to + // auto-create threads for blinded contacts if they have no messages) + // TODO: See what this will do with blinded->unblinded conversations? let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction) thread.shouldBeVisible = true thread.save(with: transaction) @@ -839,26 +843,130 @@ extension MessageReceiver { public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) { let userPublicKey = getUserHexEncodedPublicKey() + var blindedContactIds: [String] = [] + var blindedThreadIds: [String] = [] // Ignore messages which were sent from the current user guard message.sender != userPublicKey else { return } guard let senderId: String = message.sender else { return } - - // Get the existing thead and notify the user - if let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction, let thread: TSContactThread = TSContactThread.getWithContactSessionID(senderId, transaction: transaction) { - let infoMessage = TSInfoMessage( - timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()), - in: thread, - messageType: .messageRequestAccepted - ) - infoMessage.save(with: transaction) + guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { + return } + // Prep the unblinded thread + let unblindedThreadId: String = TSContactThread.threadID(fromContactSessionID: senderId) + let unblindedThread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: senderId, transaction: transaction) + + // Need to handle a `MessageRequestResponse` sent to a blinded thread (ie. check if the sender matches + // the blinded ids of any threads) + let messageRequestThreads: [String: TSContactThread] = Storage.shared.getAllMessageRequestThreads(using: transaction) + + if !messageRequestThreads.isEmpty { + var interactionsToMove: [TSInteraction] = [] + var threadsToDelete: [TSContactThread] = [] + + // Loop through all blinded threads and extract any interactions relating to the user accepting + // the message request + for blindedThread in messageRequestThreads.values { + let blindedId: String = blindedThread.contactSessionID() + + // If the sessionId matches the blindedId then this thread needs to be converted to an un-blinded thread + guard let serverPublicKey: String = blindedThread.originalOpenGroupPublicKey else { continue } + guard Sodium().sessionId(senderId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { continue } + guard let blindedThreadId: String = blindedThread.uniqueId else { continue } + guard let view: YapDatabaseAutoViewTransaction = transaction.ext(TSMessageDatabaseViewExtensionName) as? YapDatabaseAutoViewTransaction else { + continue + } + + // Cache the mapping + let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: senderId, serverPublicKey: serverPublicKey) + Storage.shared.cacheBlindedIdMapping(mapping, using: transaction) + + // Add the `blindedId` to an array so we can remove them at the end of processing + blindedContactIds.append(blindedId) + blindedThreadIds.append(blindedThreadId) + + // Loop through all of the interactions and add them to a list to be moved to the new thread + view.enumerateRows(inGroup: blindedThreadId) { _, _, object, _, _, _ in + guard let interaction: TSInteraction = object as? TSInteraction else { + return + } + + interactionsToMove.append(interaction) + } + + threadsToDelete.append(blindedThread) + + // TODO: Pending jobs??? +// Storage.shared.getAllPendingJobs(of: <#T##Job.Type#>) + } + + // Sort the interactions by their `sortId` (which looks to be a global sort id for all interactions) just in case + // the behaviour changes in the future and the value can get reset (this way we process the interactions in the + // correct order regardless of how many threads they came from) + let sortedInteractionsToMove: [TSInteraction] = interactionsToMove + .sorted { lhs, rhs -> Bool in lhs.sortId < rhs.sortId } + + // Note: Unfortunately we need to move the interactions separately from enumerating them to avoid mutating the + // `TSMessageDatabaseViewExtensionName` while enumerating it (this does mean paying the cost of looping a second time) + for interaction in sortedInteractionsToMove { + interaction.moveToThread(withId: unblindedThreadId) + interaction.save(with: transaction) + } + + // Delete the old threads + for thread in threadsToDelete { + // TODO: This isn't updating the HomeVC... Race condition??? (Seems to not happen when stepping through with breakpoints) + thread.removeAllThreadInteractions(with: transaction) + thread.remove(with: transaction) + } + } + + // Update the `didApproveMe` state of the sender updateContactApprovalStatusIfNeeded( senderSessionId: senderId, threadId: nil, - forceConfigSync: true, + forceConfigSync: blindedContactIds.isEmpty, // Sync here if there are no blinded contacts using: transaction ) + + // If there were blinded contacts then we should remove them + if !blindedContactIds.isEmpty { + // Delete all of the processed blinded contacts (shouldn't need them anymore and don't want them taking up + // space in the config message) + for blindedId in blindedContactIds { + // TODO: OWSBlockingManager...??? + } + + // We should assume the 'sender' is a newly created contact and hence need to update it's `isApproved` state + updateContactApprovalStatusIfNeeded( + senderSessionId: userPublicKey, + threadId: unblindedThreadId, + forceConfigSync: true, + using: transaction + ) + } + + // Notify the user of their approval (Note: This will always appear in the un-blinded thread) + // Note: We want to do this last as it'll mean the un-blinded thread gets updated and the contact approval status + // will have been updated at this point (which will mean the `TSThread.isMessageRequest` will return correctly + // after this is saved + let infoMessage = TSInfoMessage( + timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()), + in: unblindedThread, + messageType: .messageRequestAccepted + ) + infoMessage.save(with: transaction) + + // Finally we need to send a notification that the thread was replaced so we can handle the case where the + // user might currently have the replaced thread open (only need to do this if we actually had blindedIds) + if !blindedThreadIds.isEmpty { + let userInfo: [NotificationUserInfoKey: Any] = [ + .threadId: unblindedThreadId, + .removedThreadIds: blindedThreadIds + ] + + NotificationCenter.default.post(name: .contactThreadReplaced, object: nil, userInfo: userInfo) + } } } diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 2937c9de6..c73373d1e 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -60,7 +60,7 @@ public protocol SessionMessagingKitStorageProtocol { func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any) func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? - func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) + func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) // MARK: - -- Open Group Public Keys diff --git a/SessionMessagingKit/Threads/Notification+Thread.swift b/SessionMessagingKit/Threads/Notification+Thread.swift index 4b61f8f1b..aad4c478f 100644 --- a/SessionMessagingKit/Threads/Notification+Thread.swift +++ b/SessionMessagingKit/Threads/Notification+Thread.swift @@ -4,6 +4,7 @@ public extension Notification.Name { static let groupThreadUpdated = Notification.Name("groupThreadUpdated") static let muteSettingUpdated = Notification.Name("muteSettingUpdated") static let messageSentStatusDidChange = Notification.Name("messageSentStatusDidChange") + static let contactThreadReplaced = Notification.Name("contactThreadReplaced") } @objc public extension NSNotification { @@ -12,3 +13,8 @@ public extension Notification.Name { @objc static let muteSettingUpdated = Notification.Name.muteSettingUpdated.rawValue as NSString @objc static let messageSentStatusDidChange = Notification.Name.messageSentStatusDidChange.rawValue as NSString } + +public enum NotificationUserInfoKey: String { + case threadId + case removedThreadIds +} diff --git a/SessionMessagingKit/Threads/TSContactThread.h b/SessionMessagingKit/Threads/TSContactThread.h index f40a7b98c..8a514c75b 100644 --- a/SessionMessagingKit/Threads/TSContactThread.h +++ b/SessionMessagingKit/Threads/TSContactThread.h @@ -10,13 +10,24 @@ extern NSString *const TSContactThreadPrefix; @interface TSContactThread : TSThread +@property (nonatomic, nullable) NSString *originalOpenGroupServer; +@property (nonatomic, nullable) NSString *originalOpenGroupPublicKey; + - (instancetype)initWithContactSessionID:(NSString *)contactSessionID; + (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID NS_SWIFT_NAME(getOrCreateThread(contactSessionID:)); ++ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID + openGroupServer:(NSString *)openGroupServer + openGroupPublicKey:(NSString *)openGroupPublicKey NS_SWIFT_NAME(getOrCreateThread(contactSessionID:openGroupServer:openGroupPublicKey:)); + (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadWriteTransaction *)transaction; ++ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID + openGroupServer:(NSString *)openGroupServer + openGroupPublicKey:(NSString *)openGroupPublicKey + transaction:(YapDatabaseReadWriteTransaction *)transaction; + // Unlike getOrCreateThreadWithContactSessionID, this will _NOT_ create a thread if one does not already exist. + (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction; diff --git a/SessionMessagingKit/Threads/TSContactThread.m b/SessionMessagingKit/Threads/TSContactThread.m index 0a2a1e9b6..5c4a9459c 100644 --- a/SessionMessagingKit/Threads/TSContactThread.m +++ b/SessionMessagingKit/Threads/TSContactThread.m @@ -33,6 +33,23 @@ NSString *const TSContactThreadPrefix = @"c"; return thread; } ++ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID + openGroupServer:(NSString *)openGroupServer + openGroupPublicKey:(NSString *)openGroupPublicKey + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + TSContactThread *thread = [self fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction]; + + if (!thread) { + thread = [[TSContactThread alloc] initWithContactSessionID:contactSessionID]; + thread.originalOpenGroupServer = openGroupServer; + thread.originalOpenGroupPublicKey = openGroupPublicKey; + [thread saveWithTransaction:transaction]; + } + + return thread; +} + + (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID { __block TSContactThread *thread; @@ -43,6 +60,18 @@ NSString *const TSContactThreadPrefix = @"c"; return thread; } ++ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID + openGroupServer:(NSString *)openGroupServer + openGroupPublicKey:(NSString *)openGroupPublicKey +{ + __block TSContactThread *thread; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + thread = [self getOrCreateThreadWithContactSessionID:contactSessionID openGroupServer:openGroupServer openGroupPublicKey:openGroupPublicKey transaction:transaction]; + }]; + + return thread; +} + + (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction; { return [TSContactThread fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction]; diff --git a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift index 1128e0f9c..535a1a43f 100644 --- a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift +++ b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift @@ -86,7 +86,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { func getOpenGroup(for threadID: String) -> OpenGroup? { return (mockData[.openGroup] as? OpenGroup) } func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) { mockData[.openGroup] = openGroup } func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? { return mockData[.openGroupServer] as? OpenGroupAPI.Server } - func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { mockData[.openGroupServer] = server } + func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { mockData[.openGroupServer] = server } func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64? { return (mockData[.openGroupUserCount] as? UInt64) diff --git a/SignalUtilitiesKit/Database/Migrations/SOGSV4Migration.swift b/SignalUtilitiesKit/Database/Migrations/SOGSV4Migration.swift new file mode 100644 index 000000000..2737611b1 --- /dev/null +++ b/SignalUtilitiesKit/Database/Migrations/SOGSV4Migration.swift @@ -0,0 +1,33 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +@objc(SNSOGSV4Migration) +public class SOGSV4Migration: OWSDatabaseMigration { + + @objc + class func migrationId() -> String { + return "003" + } + + override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) { + self.doMigrationAsync(completion: completion) + } + + private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) { + // These collections became redundant in SOGS V4 + let lastMessageServerIDCollection: String = "SNLastMessageServerIDCollection" + let lastDeletionServerIDCollection: String = "SNLastDeletionServerIDCollection" + let authTokenCollection: String = "SNAuthTokenCollection" + + Storage.write(with: { transaction in + transaction.removeAllObjects(inCollection: lastMessageServerIDCollection) + transaction.removeAllObjects(inCollection: lastDeletionServerIDCollection) + transaction.removeAllObjects(inCollection: authTokenCollection) + + self.save(with: transaction) // Intentionally capture self + }, completion: { + completion() + }) + } +}