From 631633b76a413a8c6cff35da8353aede3ad5d2c2 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 22 Oct 2021 19:12:34 -0300 Subject: [PATCH] Merge branch 'charlesmchen/nse2021.10.21.2' --- Pods | 2 +- Signal.xcodeproj/project.pbxproj | 4 + .../Individual/IndividualCallService.swift | 3 +- .../ContactViewController.swift | 4 +- .../CV/CVComponentState.swift | 2 +- .../CVComponentSystemMessage.swift | 3 +- .../ConversationViewController+UI.swift | 5 +- .../DebugUI/DebugUIGroupsV2.swift | 4 +- .../HomeView/HomeViewController.m | 63 +-- .../RecipientPickerViewController.m | 3 +- ...ationSettingsViewController+Contents.swift | 4 +- .../ThreadSettings/MemberActionSheet.swift | 5 +- ...ameCollisionResolutionViewController.swift | 4 +- .../OWSAddToContactViewController.swift | 4 +- .../src/views/NameCollisionReviewCell.swift | 2 +- .../util/FTS/GRDBFullTextSearcherTest.swift | 24 +- .../test/util/GroupAndContactStreamTest.swift | 24 +- SignalMessaging/contacts/ContactsState.swift | 382 ++++++++++++++++++ SignalMessaging/contacts/OWSContactsManager.h | 21 +- SignalMessaging/contacts/OWSContactsManager.m | 309 +++++--------- .../contacts/OWSContactsManager.swift | 93 ++++- SignalMessaging/contacts/OWSSyncManager.m | 7 +- SignalServiceKit/src/Contacts/Contact.h | 3 + SignalServiceKit/src/Contacts/Contact.m | 8 - SignalServiceKit/src/Contacts/Contact.swift | 38 ++ .../src/Messages/Interactions/OWSContact.h | 2 +- .../src/Messages/Interactions/OWSContact.m | 5 +- SignalServiceKit/src/Messages/MessageSender.m | 2 +- .../src/Messages/UD/OWSUDManager.swift | 5 +- .../src/Protocols/ContactsManagerProtocol.h | 14 +- .../Storage/Database/SDSKeyValueStore.swift | 2 + .../src/TestUtils/FakeContactsManager.swift | 20 +- SignalServiceKit/src/Util/LRUCache.swift | 2 +- SignalServiceKit/src/Util/LocalDevice.swift | 15 +- .../src/groups/GroupManager.swift | 3 +- .../tests/Messages/OWSUDManagerTest.swift | 8 +- SignalUI/Utils/BlockListUIUtils.m | 4 +- SignalUI/Utils/FullTextSearcher.swift | 2 +- .../ViewModels/ContactShareViewModel.swift | 4 +- SignalUI/Views/ContactsViewHelper.m | 39 +- 40 files changed, 810 insertions(+), 338 deletions(-) create mode 100644 SignalMessaging/contacts/ContactsState.swift create mode 100644 SignalServiceKit/src/Contacts/Contact.swift diff --git a/Pods b/Pods index 5eb2e77c47..f6c13f81b9 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 5eb2e77c4768eed576f576a9eaf0b07fbfa865a2 +Subproject commit f6c13f81b9571440b2cf512c44089e36333854ed diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 2ddc970507..808781cecf 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -240,6 +240,7 @@ 342FFE7A271EF581000AC89F /* UIResponder+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342FFE73271EF580000AC89F /* UIResponder+OWS.swift */; }; 342FFE7B271EF581000AC89F /* UIStoryboard+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342FFE74271EF580000AC89F /* UIStoryboard+OWS.swift */; }; 342FFE7E271EF5B2000AC89F /* ReturnToCallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342FFE7D271EF5B1000AC89F /* ReturnToCallViewController.swift */; }; + 342FFE832721D4B6000AC89F /* ContactsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342FFE822721D4B6000AC89F /* ContactsState.swift */; }; 34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; }; 343417F12530A7480034FE0C /* CVComponentReactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343417F02530A7480034FE0C /* CVComponentReactions.swift */; }; 3434AE1C22AEDE7D002EE04E /* ViewOnceMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3434AE1B22AEDE7D002EE04E /* ViewOnceMessageViewController.swift */; }; @@ -1235,6 +1236,7 @@ 342FFE73271EF580000AC89F /* UIResponder+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+OWS.swift"; sourceTree = ""; }; 342FFE74271EF580000AC89F /* UIStoryboard+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard+OWS.swift"; sourceTree = ""; }; 342FFE7D271EF5B1000AC89F /* ReturnToCallViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReturnToCallViewController.swift; sourceTree = ""; }; + 342FFE822721D4B6000AC89F /* ContactsState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsState.swift; sourceTree = ""; }; 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fontawesome-webfont.ttf"; sourceTree = ""; }; 343417F02530A7480034FE0C /* CVComponentReactions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CVComponentReactions.swift; sourceTree = ""; }; 3434AE1B22AEDE7D002EE04E /* ViewOnceMessageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewOnceMessageViewController.swift; sourceTree = ""; }; @@ -2747,6 +2749,7 @@ 346129A11FD1F09100532771 /* contacts */ = { isa = PBXGroup; children = ( + 342FFE822721D4B6000AC89F /* ContactsState.swift */, 4C9D347E23689E06006A4307 /* IncomingContactSyncJobQueue.swift */, 4C6E446822AEDDEE007982E6 /* NewAccountDiscovery.swift */, 346129A21FD1F09100532771 /* OWSContactsManager.h */, @@ -5721,6 +5724,7 @@ files = ( 88D23D0C23CEBF4400B0E74B /* AppNotifications.swift in Sources */, 4CB93DC22180FF07004B9764 /* ProximityMonitoringManager.swift in Sources */, + 342FFE832721D4B6000AC89F /* ContactsState.swift in Sources */, 88D7BA9E266809F50088D1C2 /* CallMessageRelay.swift in Sources */, 4C046AA7236148880035B234 /* OWSGroupSyncProcessingJobQueue.swift in Sources */, 4C9D347F23689E06006A4307 /* IncomingContactSyncJobQueue.swift in Sources */, diff --git a/Signal/src/Calls/Individual/IndividualCallService.swift b/Signal/src/Calls/Individual/IndividualCallService.swift index 2667920e0c..f69afb3768 100644 --- a/Signal/src/Calls/Individual/IndividualCallService.swift +++ b/Signal/src/Calls/Individual/IndividualCallService.swift @@ -198,7 +198,8 @@ import SignalMessaging // * The thread belongs to someone in our system contacts // * The thread existed before messages requests return (self.profileManager.isThread(inProfileWhitelist: thread, transaction: transaction) - || self.contactsManager.isSystemContact(address: thread.contactAddress) + || self.contactsManager.isSystemContact(address: thread.contactAddress, + transaction: transaction) || GRDBThreadFinder.isPreMessageRequestsThread(thread, transaction: transaction.unwrapGrdbRead)) } diff --git a/Signal/src/ViewControllers/ContactViewController.swift b/Signal/src/ViewControllers/ContactViewController.swift index d16b4c5689..4bdc2a8225 100644 --- a/Signal/src/ViewControllers/ContactViewController.swift +++ b/Signal/src/ViewControllers/ContactViewController.swift @@ -138,7 +138,9 @@ class ContactViewController: OWSViewController, ContactShareViewHelperDelegate { private func systemContactsForContact() -> [String] { AssertIsOnMainThread() - return contactShare.systemContactPhoneNumbers() + return databaseStorage.read { transaction in + contactShare.systemContactPhoneNumbers(transaction: transaction) + } } private func updateContent() { diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift index 3c5b65f461..e82fb18946 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift @@ -883,7 +883,7 @@ fileprivate extension CVComponentState.Builder { self.contactShare = ContactShare(state: state) let hasSendTextButton = !contactShare.systemContactsWithSignalAccountPhoneNumbers(transaction: transaction).isEmpty - let hasInviteButton = !contactShare.systemContactPhoneNumbers().isEmpty + let hasInviteButton = !contactShare.systemContactPhoneNumbers(transaction: transaction).isEmpty let hasAddToContactsButton = !contactShare.e164PhoneNumbers().isEmpty if hasSendTextButton { diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift index dd74339f7f..30eee95103 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift @@ -1084,7 +1084,8 @@ extension CVComponentSystemMessage { guard let profileChangeNewNameComponents = infoMessage.profileChangeNewNameComponents else { return nil } - guard Self.contactsManager.isSystemContact(address: profileChangeAddress) else { + guard Self.contactsManager.isSystemContact(address: profileChangeAddress, + transaction: transaction) else { return nil } let systemContactName = Self.contactsManagerImpl.nameFromSystemContacts(for: profileChangeAddress, diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController+UI.swift b/Signal/src/ViewControllers/ConversationView/ConversationViewController+UI.swift index 41285761e3..4792450662 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController+UI.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController+UI.swift @@ -21,7 +21,10 @@ extension ConversationViewController { } // If the user is in the system contacts, show a badge - if self.contactsManagerImpl.isSystemContact(address: contactThread.contactAddress) { + let isSystemContact = databaseStorage.read { transaction in + contactsManagerImpl.isSystemContact(address: contactThread.contactAddress, transaction: transaction) + } + if isSystemContact { icon = UIImage(named: "contact-outline-16")?.withRenderingMode(.alwaysTemplate) } } else if let groupThread = thread as? TSGroupThread { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift b/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift index 61ad72e54c..4bf1596e17 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift @@ -153,10 +153,12 @@ class DebugUIGroupsV2: DebugUIPage { let prefix = prefix + " " + let signalAccounts = self.contactsManagerImpl.unsortedSignalAccounts(transaction: transaction) + // These will fail if you aren't registered or // don't have some Signal users in your contacts. let localAddress = tsAccountManager.localAddress! - var allAddresses = contactsManagerImpl.signalAccounts.map { $0.recipientAddress } + var allAddresses = signalAccounts.map { $0.recipientAddress } if groupsVersion == .V2 { // V2 group members must have a uuid. allAddresses = allAddresses.filter { $0.uuid != nil } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 106f79e13a..0f27412723 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -277,9 +277,12 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; - (NSArray *)suggestedAccountsForFirstContact { + // Load all signal accounts even though we only need the first N; + // we want the returned value to be stable so we need to sort. + NSArray *sortedSignalAccounts = [self.contactsManagerImpl sortedSignalAccountsWithSneakyTransaction]; + NSMutableArray *accounts = [NSMutableArray new]; - - for (SignalAccount *account in self.contactsManager.signalAccounts) { + for (SignalAccount *account in sortedSignalAccounts) { if (account.recipientAddress.isLocalAddress) { continue; } @@ -294,33 +297,41 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; - (void)updateFirstConversationLabel { - NSArray *signalAccounts = self.suggestedAccountsForFirstContact; - NSString *formatString = @""; + __block NSString *formatString = @""; NSMutableArray *contactNames = [NSMutableArray new]; - if (signalAccounts.count >= 3) { - [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]]; - [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[1]]]; - [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[2]]]; - - formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_3_CONTACTS_FORMAT", - @"Format string for a label offering to start a new conversation with your contacts, if you have at least " - @"3 Signal contacts. Embeds {{The names of 3 of your Signal contacts}}."); - } else if (signalAccounts.count == 2) { - [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]]; - [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[1]]]; - - formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_2_CONTACTS_FORMAT", - @"Format string for a label offering to start a new conversation with your contacts, if you have 2 Signal " - @"contacts. Embeds {{The names of 2 of your Signal contacts}}."); - } else if (signalAccounts.count == 1) { - [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]]; - - formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_1_CONTACT_FORMAT", - @"Format string for a label offering to start a new conversation with your contacts, if you have 1 Signal " - @"contact. Embeds {{The name of 1 of your Signal contacts}}."); - } + + [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { + if (signalAccounts.count >= 3) { + [contactNames addObject:[self.contactsManagerImpl displayNameForSignalAccount:signalAccounts[0] + transaction:transaction]]; + [contactNames addObject:[self.contactsManagerImpl displayNameForSignalAccount:signalAccounts[1] + transaction:transaction]]; + [contactNames addObject:[self.contactsManagerImpl displayNameForSignalAccount:signalAccounts[2] + transaction:transaction]]; + + formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_3_CONTACTS_FORMAT", + @"Format string for a label offering to start a new conversation with your contacts, if you have at least " + @"3 Signal contacts. Embeds {{The names of 3 of your Signal contacts}}."); + } else if (signalAccounts.count == 2) { + [contactNames addObject:[self.contactsManagerImpl displayNameForSignalAccount:signalAccounts[0] + transaction:transaction]]; + [contactNames addObject:[self.contactsManagerImpl displayNameForSignalAccount:signalAccounts[1] + transaction:transaction]]; + + formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_2_CONTACTS_FORMAT", + @"Format string for a label offering to start a new conversation with your contacts, if you have 2 Signal " + @"contacts. Embeds {{The names of 2 of your Signal contacts}}."); + } else if (signalAccounts.count == 1) { + [contactNames addObject:[self.contactsManagerImpl displayNameForSignalAccount:signalAccounts[0] + transaction:transaction]]; + + formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_1_CONTACT_FORMAT", + @"Format string for a label offering to start a new conversation with your contacts, if you have 1 Signal " + @"contact. Embeds {{The name of 1 of your Signal contacts}}."); + } + }]; NSString *embedToken = @"%@"; NSArray *formatSplits = [formatString componentsSeparatedByString:embedToken]; diff --git a/Signal/src/ViewControllers/RecipientPicker/RecipientPickerViewController.m b/Signal/src/ViewControllers/RecipientPicker/RecipientPickerViewController.m index 293002552a..d3ad66b577 100644 --- a/Signal/src/ViewControllers/RecipientPicker/RecipientPickerViewController.m +++ b/Signal/src/ViewControllers/RecipientPicker/RecipientPickerViewController.m @@ -31,8 +31,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)stringForCollation { - OWSContactsManager *contactsManager = Environment.shared.contactsManager; - return [contactsManager comparableNameForSignalAccount:self]; + return [self.contactsManagerImpl comparableNameForSignalAccountWithSneakyTransaction:self]; } @end diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift index 94c92cb5bf..a52abd4878 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift @@ -26,7 +26,9 @@ extension ConversationSettingsViewController { owsFailDebug("Invalid thread.") return false } - return contactsManagerImpl.isSystemContact(address: contactThread.contactAddress) + return databaseStorage.read { transaction in + contactsManagerImpl.isSystemContact(address: contactThread.contactAddress, transaction: transaction) + } } // MARK: - Table diff --git a/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift b/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift index cd68ea7209..a323c81b46 100644 --- a/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift +++ b/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift @@ -257,7 +257,10 @@ class MemberActionSheet: InteractiveSheetViewController { )) if contactsManagerImpl.supportsContactEditing { - if contactsManager.isSystemContact(address: address) { + let isSystemContact = databaseStorage.read { transaction in + contactsManager.isSystemContact(address: address, transaction: transaction) + } + if isSystemContact { section.add(.actionItem( icon: .settingsUserInContacts, name: NSLocalizedString( diff --git a/Signal/src/ViewControllers/ThreadSettings/NameCollisionResolutionViewController.swift b/Signal/src/ViewControllers/ThreadSettings/NameCollisionResolutionViewController.swift index 53dce1a13d..89a708ca2a 100644 --- a/Signal/src/ViewControllers/ThreadSettings/NameCollisionResolutionViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/NameCollisionResolutionViewController.swift @@ -300,7 +300,9 @@ class NameCollisionResolutionViewController: OWSTableViewController2 { extension NameCollisionResolutionViewController: CNContactViewControllerDelegate, ContactsViewHelperObserver { func shouldShowContactUpdateAction(for address: SignalServiceAddress) -> Bool { - return contactsManager.isSystemContact(address: address) && contactsManagerImpl.supportsContactEditing + databaseStorage.read { transaction in + contactsManager.isSystemContact(address: address, transaction: transaction) && contactsManagerImpl.supportsContactEditing + } } func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) { diff --git a/Signal/src/ViewControllers/ThreadSettings/OWSAddToContactViewController.swift b/Signal/src/ViewControllers/ThreadSettings/OWSAddToContactViewController.swift index 228fbbce81..b74f7d8fb9 100644 --- a/Signal/src/ViewControllers/ThreadSettings/OWSAddToContactViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/OWSAddToContactViewController.swift @@ -84,7 +84,9 @@ public class OWSAddToContactViewController: OWSViewController { } private func updateData() { - contacts = contactsManagerImpl.allContacts + contacts = databaseStorage.read { transaction in + contactsManagerImpl.allSortedContacts(transaction: transaction) + } } } diff --git a/Signal/src/views/NameCollisionReviewCell.swift b/Signal/src/views/NameCollisionReviewCell.swift index 72552e0831..be9ec869e0 100644 --- a/Signal/src/views/NameCollisionReviewCell.swift +++ b/Signal/src/views/NameCollisionReviewCell.swift @@ -80,7 +80,7 @@ extension NameCollision { commonGroupsString: commonGroupsString(for: $0.address, thread: thread, transaction: transaction), avatar: avatar(for: $0.address, transaction: transaction), isBlocked: blockingManager.isAddressBlocked($0.address), - isSystemContact: Self.contactsManager.isSystemContact(address: $0.address) + isSystemContact: Self.contactsManager.isSystemContact(address: $0.address, transaction: transaction) ) } } diff --git a/Signal/test/util/FTS/GRDBFullTextSearcherTest.swift b/Signal/test/util/FTS/GRDBFullTextSearcherTest.swift index e80edd5c14..e88f93b23d 100644 --- a/Signal/test/util/FTS/GRDBFullTextSearcherTest.swift +++ b/Signal/test/util/FTS/GRDBFullTextSearcherTest.swift @@ -62,10 +62,6 @@ class GRDBFullTextSearcherContactsManager: NSObject, ContactsManagerProtocol { "Fake name" } - func nameComponents(for address: SignalServiceAddress) -> PersonNameComponents? { - PersonNameComponents() - } - func nameComponents(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> PersonNameComponents? { PersonNameComponents() } @@ -74,12 +70,20 @@ class GRDBFullTextSearcherContactsManager: NSObject, ContactsManagerProtocol { [] } - func isSystemContact(phoneNumber: String) -> Bool { - true + func isSystemContactWithSneakyTransaction(phoneNumber: String) -> Bool { + return true } - func isSystemContact(address: SignalServiceAddress) -> Bool { - true + func isSystemContact(phoneNumber: String, transaction: SDSAnyReadTransaction) -> Bool { + return true + } + + func isSystemContactWithSneakyTransaction(address: SignalServiceAddress) -> Bool { + return true + } + + func isSystemContact(address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> Bool { + return true } func isSystemContact(withSignalAccount recipientId: String) -> Bool { @@ -90,10 +94,6 @@ class GRDBFullTextSearcherContactsManager: NSObject, ContactsManagerProtocol { true } - func hasNameInSystemContacts(for address: SignalServiceAddress) -> Bool { - (mockDisplayNameMap[address] ?? "").count > 0 - } - func compare(signalAccount left: SignalAccount, with right: SignalAccount) -> ComparisonResult { owsFailDebug("if this method ends up being used by the tests, we should provide a better implementation.") diff --git a/Signal/test/util/GroupAndContactStreamTest.swift b/Signal/test/util/GroupAndContactStreamTest.swift index d45ea102b6..7472421e1d 100644 --- a/Signal/test/util/GroupAndContactStreamTest.swift +++ b/Signal/test/util/GroupAndContactStreamTest.swift @@ -361,10 +361,6 @@ class TestContactsManager: NSObject, ContactsManagerProtocol { address.stringForDisplay } - func nameComponents(for address: SignalServiceAddress) -> PersonNameComponents? { - PersonNameComponents() - } - func nameComponents(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> PersonNameComponents? { PersonNameComponents() } @@ -373,12 +369,20 @@ class TestContactsManager: NSObject, ContactsManagerProtocol { [] } - func isSystemContact(phoneNumber: String) -> Bool { - true + func isSystemContactWithSneakyTransaction(phoneNumber: String) -> Bool { + return true } - func isSystemContact(address: SignalServiceAddress) -> Bool { - true + func isSystemContact(phoneNumber: String, transaction: SDSAnyReadTransaction) -> Bool { + return true + } + + func isSystemContactWithSneakyTransaction(address: SignalServiceAddress) -> Bool { + return true + } + + func isSystemContact(address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> Bool { + return true } func isSystemContact(withSignalAccount phoneNumber: String) -> Bool { @@ -389,10 +393,6 @@ class TestContactsManager: NSObject, ContactsManagerProtocol { true } - func hasNameInSystemContacts(for address: SignalServiceAddress) -> Bool { - false - } - func compare(signalAccount left: SignalAccount, with right: SignalAccount) -> ComparisonResult { .orderedSame } diff --git a/SignalMessaging/contacts/ContactsState.swift b/SignalMessaging/contacts/ContactsState.swift new file mode 100644 index 0000000000..f0343877fa --- /dev/null +++ b/SignalMessaging/contacts/ContactsState.swift @@ -0,0 +1,382 @@ +// +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalServiceKit + +@objc +public protocol ContactsState: AnyObject { + func unsortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] + + // Order respects the systems contact sorting preference. + func sortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] + + func setSortedSignalAccounts(_ signalAccounts: [SignalAccount]) + + func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? + + func allContacts(transaction: SDSAnyReadTransaction) -> [Contact] + + func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps + + func setContactsMaps(_ contactsMaps: ContactsMaps, + localNumber: String?, + transaction: SDSAnyWriteTransaction) + + func warmCaches(transaction: SDSAnyReadTransaction) -> ContactsStateSummary +} + +// MARK: - + +@objc +public class ContactsStateSummary: NSObject { + @objc + public let contactCount: UInt + @objc + public let signalAccountCount: UInt + + required init(contactCount: UInt, signalAccountCount: UInt) { + self.contactCount = contactCount + self.signalAccountCount = signalAccountCount + } +} + +// MARK: - + +@objc +public class ContactsMaps: NSObject { + @objc + public let uniqueIdToContactMap: [String: Contact] + @objc + public let phoneNumberToContactMap: [String: Contact] + + @objc + public var allContacts: [Contact] { Array(uniqueIdToContactMap.values) } + + required init(uniqueIdToContactMap: [String: Contact], + phoneNumberToContactMap: [String: Contact]) { + self.uniqueIdToContactMap = uniqueIdToContactMap + self.phoneNumberToContactMap = phoneNumberToContactMap + } + + static let empty: ContactsMaps = ContactsMaps(uniqueIdToContactMap: [:], phoneNumberToContactMap: [:]) + + // Builds a map of phone number-to-Contact. + // A given Contact may have multiple phone numbers. + @objc + public static func build(contacts: [Contact], + localNumber: String?) -> ContactsMaps { + + var uniqueIdToContactMap = [String: Contact]() + var phoneNumberToContactMap = [String: Contact]() + for contact in contacts { + let phoneNumbers = Self.phoneNumbers(forContact: contact, localNumber: localNumber) + guard !phoneNumbers.isEmpty else { + continue + } + + uniqueIdToContactMap[contact.uniqueId] = contact + + for phoneNumber in phoneNumbers { + phoneNumberToContactMap[phoneNumber] = contact + } + } + return ContactsMaps(uniqueIdToContactMap: uniqueIdToContactMap, + phoneNumberToContactMap: phoneNumberToContactMap) + } + + fileprivate static func phoneNumbers(forContact contact: Contact, localNumber: String?) -> [String] { + return contact.parsedPhoneNumbers.compactMap { phoneNumber in + guard let phoneNumberE164 = phoneNumber.toE164().nilIfEmpty else { + return nil + } + + // Ignore any system contact records for the local contact. + // For the local user we never want to show the avatar / + // name that you have entered for yourself in your system + // contacts. Instead, we always want to display your profile + // name and avatar. + let isLocalContact = phoneNumberE164 == localNumber + guard !isLocalContact else { + return nil + } + + return phoneNumberE164 + } + } + + @objc + public func isSystemContact(address: SignalServiceAddress) -> Bool { + guard let phoneNumber = address.phoneNumber?.nilIfEmpty else { + return false + } + return isSystemContact(phoneNumber: phoneNumber) + } + + @objc + public func isSystemContact(phoneNumber: String) -> Bool { + phoneNumberToContactMap[phoneNumber] != nil + } + + public func isEqualForCache(_ other: ContactsMaps) -> Bool { + let mapSelf = self.uniqueIdToContactMap + let mapOther = other.uniqueIdToContactMap + let keysSelf = Set(mapSelf.keys) + let keysOther = Set(mapOther.keys) + guard keysSelf == keysOther else { + return false + } + for key in keysSelf { + guard let valueSelf = mapSelf[key], + let valueOther = mapOther[key], + valueSelf.isEqualForCache(valueOther) else { + return false + } + } + return true + } +} + +// MARK: - + +@objc +public class ContactsStateInDatabase: NSObject, ContactsState { + @objc + public func unsortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] { + SignalAccount.anyFetchAll(transaction: transaction) + } + + // Order respects the systems contact sorting preference. + @objc + public func sortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] { + contactsManagerImpl.sortSignalAccounts(unsortedSignalAccounts(transaction: transaction), + transaction: transaction) + } + + @objc + public func setSortedSignalAccounts(_ signalAccounts: [SignalAccount]) { + // Ignore. + } + + private static let uniqueIdStore = SDSKeyValueStore(collection: "ContactsState.uniqueIdStore") + private static let phoneNumberStore = SDSKeyValueStore(collection: "ContactsState.phoneNumberStore") + + private static func serializeContact(_ contact: Contact, label: String) -> Data? { + let data = NSKeyedArchiver.archivedData(withRootObject: contact) + guard !data.isEmpty else { + owsFailDebug("Could not serialize contact: \(label).") + return nil + } + return data + } + + private static func deserializeContact(data: Data, label: String) -> Contact? { + do { + guard let contact = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Contact else { + owsFailDebug("Invalid value: \(label).") + return nil + } + return contact + } catch { + owsFailDebug("Deserialize failed[\(label)]: \(error).") + return nil + } + } + + @objc + public func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? { + guard let data = Self.phoneNumberStore.getData(phoneNumber, transaction: transaction) else { + return nil + } + return Self.deserializeContact(data: data, label: phoneNumber) + } + + @objc + public func allContacts(transaction: SDSAnyReadTransaction) -> [Contact] { + var contacts = [Contact]() + Self.uniqueIdStore.enumerateKeys(transaction: transaction) { (key, _) in + guard let data = Self.uniqueIdStore.getData(key, transaction: transaction) else { + owsFailDebug("Missing data for key: \(key).") + return + } + guard let contact = Self.deserializeContact(data: data, label: key) else { + return + } + contacts.append(contact) + } + return contacts + } + + @objc + public func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps { + let contacts = allContacts(transaction: transaction) + let localNumber: String? = tsAccountManager.localNumber(with: transaction) + return ContactsMaps.build(contacts: contacts, localNumber: localNumber) + } + + @objc + public func setContactsMaps(_ newContactsMaps: ContactsMaps, + localNumber: String?, + transaction: SDSAnyWriteTransaction) { + + let oldContactsMaps = self.contactsMaps(transaction: transaction) + if oldContactsMaps.isEqualForCache(newContactsMaps) { + Logger.verbose("Ignoring redundant contactsMap update.") + return + } + + Self.uniqueIdStore.removeAll(transaction: transaction) + Self.phoneNumberStore.removeAll(transaction: transaction) + + for contact in newContactsMaps.uniqueIdToContactMap.values { + let phoneNumbers = ContactsMaps.phoneNumbers(forContact: contact, localNumber: localNumber) + guard !phoneNumbers.isEmpty else { + continue + } + guard let contactData = Self.serializeContact(contact, label: contact.uniqueId) else { + continue + } + + Self.uniqueIdStore.setData(contactData, key: contact.uniqueId, transaction: transaction) + + for phoneNumber in phoneNumbers { + Self.phoneNumberStore.setData(contactData, key: phoneNumber, transaction: transaction) + } + } + } + + @objc + public func warmCaches(transaction: SDSAnyReadTransaction) -> ContactsStateSummary { + // Only load the summary. + let contactCount = Self.uniqueIdStore.numberOfKeys(transaction: transaction) + let signalAccountCount = SignalAccount.anyCount(transaction: transaction) + return ContactsStateSummary(contactCount: contactCount, signalAccountCount: signalAccountCount) + } +} + +// MARK: - + +@objc +public class ContactsStateInMemory: NSObject, ContactsState { + private let unfairLock = UnfairLock() + + private let contactsStateInDatabase = ContactsStateInDatabase() + + private var sortedSignalAccountsCache: [SignalAccount]? + + private var contactsMapsCache: ContactsMaps? + + @objc + public func unsortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] { + // Prefer cache. + if let cachedValue = (unfairLock.withLock { sortedSignalAccountsCache }) { + return cachedValue + } + // Fail over. + // NOTE: We use the "sorted" getter because our cache should be sorted. + let result = contactsStateInDatabase.sortedSignalAccounts(transaction: transaction) + unfairLock.withLock { sortedSignalAccountsCache = result } + return result + } + + // Order respects the systems contact sorting preference. + @objc + public func sortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] { + // Prefer cache. + if let cachedValue = (unfairLock.withLock { sortedSignalAccountsCache }) { + return cachedValue + } + // Fail over. + let result = contactsStateInDatabase.sortedSignalAccounts(transaction: transaction) + unfairLock.withLock { sortedSignalAccountsCache = result } + return result + } + + @objc + public func setSortedSignalAccounts(_ signalAccounts: [SignalAccount]) { + unfairLock.withLock { + // Update cache. + sortedSignalAccountsCache = signalAccounts + } + return contactsStateInDatabase.setSortedSignalAccounts(signalAccounts) + } + + @objc + public func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? { + unfairLock.withLock { + guard let contactsMaps = contactsMapsCache else { + owsFailDebug("Missing contactsMaps.") + // Don't bother failing over to contactsStateInDatabase. + return nil + } + return contactsMaps.phoneNumberToContactMap[phoneNumber] + } + } + + @objc + public func allContacts(transaction: SDSAnyReadTransaction) -> [Contact] { + unfairLock.withLock { + guard let contactsMaps = contactsMapsCache else { + owsFailDebug("Missing contactsMaps.") + // Don't bother failing over to contactsStateInDatabase. + return [] + } + return Array(contactsMaps.phoneNumberToContactMap.values) + } + } + + @objc + public func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps { + unfairLock.withLock { + guard let contactsMaps = contactsMapsCache else { + owsFailDebug("Missing contactsMaps.") + // Don't bother failing over to contactsStateInDatabase. + return .empty + } + return contactsMaps + } + } + + @objc + public func setContactsMaps(_ contactsMaps: ContactsMaps, + localNumber: String?, + transaction: SDSAnyWriteTransaction) { + unfairLock.withLock { + // Update cache. + contactsMapsCache = contactsMaps + } + return contactsStateInDatabase.setContactsMaps(contactsMaps, + localNumber: localNumber, + transaction: transaction) + } + + @objc + public func warmCaches(transaction: SDSAnyReadTransaction) -> ContactsStateSummary { + + let sortedSignalAccounts = contactsStateInDatabase.sortedSignalAccounts(transaction: transaction) + let contactsMaps = contactsStateInDatabase.contactsMaps(transaction: transaction) + + unfairLock.withLock { + sortedSignalAccountsCache = sortedSignalAccounts + contactsMapsCache = contactsMaps + } + + // Don't call contactsStateInDatabase.warmCaches(). + return ContactsStateSummary(contactCount: UInt(contactsMaps.uniqueIdToContactMap.count), + signalAccountCount: UInt(sortedSignalAccounts.count)) + } +} + +// MARK: - + +@objc +extension OWSContactsManager { + public func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? { + contactsState.contact(forPhoneNumber: phoneNumber, transaction: transaction) + } + + public func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps { + contactsState.contactsMaps(transaction: transaction) + } +} diff --git a/SignalMessaging/contacts/OWSContactsManager.h b/SignalMessaging/contacts/OWSContactsManager.h index f97c896522..1ca5153bfe 100644 --- a/SignalMessaging/contacts/OWSContactsManager.h +++ b/SignalMessaging/contacts/OWSContactsManager.h @@ -17,29 +17,27 @@ extern NSNotificationName const OWSContactsManagerContactsDidChangeNotification; @class SignalServiceAddress; @class UIFont; +@protocol ContactsState; + /** * Get latest Signal contacts, and be notified when they change. */ @interface OWSContactsManager : NSObject +@property (nonatomic, readonly) BOOL shouldSortByGivenName; + +@property (nonatomic, readonly) id contactsState; + #pragma mark - Accessors @property (nonatomic, readonly) SDSKeyValueStore *keyValueStore; -@property (atomic, readonly) NSArray *allContacts; - -@property (atomic, readonly) NSDictionary *allContactsMap; - -// order of the signalAccounts array respects the systems contact sorting preference -@property (atomic, readonly) NSArray *signalAccounts; - // This will return an instance of SignalAccount for _known_ signal accounts. - (nullable SignalAccount *)fetchSignalAccountForAddress:(SignalServiceAddress *)address; - (nullable SignalAccount *)fetchSignalAccountForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; -- (nullable NSString *)nameFromSystemContactsForAddress:(SignalServiceAddress *)address; - (nullable NSString *)nameFromSystemContactsForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; @@ -59,7 +57,7 @@ extern NSNotificationName const OWSContactsManagerContactsDidChangeNotification; // Not set until a contact fetch has completed. // Set even if no contacts are found. -@property (nonatomic, readonly) BOOL hasLoadedContacts; +@property (nonatomic, readonly) BOOL hasLoadedSystemContacts; // Request systems contacts and start syncing changes. The user will see an alert // if they haven't previously. @@ -77,14 +75,9 @@ extern NSNotificationName const OWSContactsManagerContactsDidChangeNotification; #pragma mark - Util -- (BOOL)isSystemContactWithPhoneNumber:(NSString *)phoneNumber; -- (BOOL)isSystemContactWithAddress:(SignalServiceAddress *)address; -- (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address; - /** * Used for sorting, respects system contacts name sort order preference. */ -- (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount; - (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount transaction:(SDSAnyReadTransaction *)transaction; - (NSString *)comparableNameForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; diff --git a/SignalMessaging/contacts/OWSContactsManager.m b/SignalMessaging/contacts/OWSContactsManager.m index af0c09a2b6..42e500bae4 100644 --- a/SignalMessaging/contacts/OWSContactsManager.m +++ b/SignalMessaging/contacts/OWSContactsManager.m @@ -31,11 +31,6 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan @interface OWSContactsManager () @property (nonatomic) BOOL isContactsUpdateInFlight; -// This reflects the contents of the device phone book and includes -// contacts that do not correspond to any signal account. -@property (atomic) NSArray *allContacts; -@property (atomic) NSDictionary *allContactsMap; -@property (atomic) NSArray *signalAccounts; @property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher; @property (nonatomic, readonly) AnyLRUCache *cnContactCache; @@ -56,14 +51,16 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan _keyValueStore = [[SDSKeyValueStore alloc] initWithCollection:OWSContactsManagerCollection]; - _allContacts = @[]; - _allContactsMap = @{}; - _signalAccounts = @[]; _systemContactsFetcher = [SystemContactsFetcher new]; _systemContactsFetcher.delegate = self; _cnContactCache = [[AnyLRUCache alloc] initWithMaxSize:50 nseMaxSize:0 shouldEvacuateInBackground:YES]; + if (CurrentAppContext().isMainApp) { + _contactsState = [ContactsStateInMemory new]; + } else { + _contactsState = [ContactsStateInDatabase new]; + } OWSSingletonAssert(); @@ -75,19 +72,14 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan } - (void)setup { - __block NSMutableArray *signalAccounts; [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { - NSUInteger signalAccountCount = [SignalAccount anyCountWithTransaction:transaction]; - OWSLogInfo(@"loading %lu signal accounts from cache.", (unsigned long)signalAccountCount); - - signalAccounts = [[NSMutableArray alloc] initWithCapacity:signalAccountCount]; - - [SignalAccount anyEnumerateWithTransaction:transaction - block:^(SignalAccount *signalAccount, BOOL *stop) { - [signalAccounts addObject:signalAccount]; - }]; + ContactsStateSummary *contactsStateSummary = [self.contactsState warmCachesWithTransaction:transaction]; + OWSLogInfo(@"Loaded %lu contacts, %lu signal accounts from cache.", + (unsigned long)contactsStateSummary.contactCount, + (unsigned long)contactsStateSummary.signalAccountCount); }]; - [self updateSignalAccounts:signalAccounts shouldSetHasLoadedContacts:NO]; + + self.isSetup = YES; } #pragma mark - System Contact Fetching @@ -240,8 +232,6 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan { OWSAssertDebug(contacts); OWSAssertDebug(completion); - OWSAssertIsOnMainThread(); - dispatch_async(self.intersectionQueue, ^{ __block BOOL isFullIntersection = YES; @@ -436,7 +426,8 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan // Retry with exponential backoff. // // TODO: Abort if another contact intersection succeeds in the meantime. - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self intersectContacts:phoneNumbers retryDelaySeconds:retryDelaySeconds * 2.0 success:successParameter @@ -445,7 +436,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan }; OWSContactDiscoveryTask *discoveryTask = [[OWSContactDiscoveryTask alloc] initWithPhoneNumbers:phoneNumbers]; [discoveryTask performAtQoS:QOS_CLASS_USER_INITIATED - callbackQueue:dispatch_get_main_queue() + callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) success:success failure:failure]; } @@ -456,107 +447,86 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan shouldClearStaleCache:(BOOL)shouldClearStaleCache { dispatch_async(self.intersectionQueue, ^{ - NSMutableArray *allContacts = [contacts mutableCopy]; - NSMutableDictionary *allContactsMap = [NSMutableDictionary new]; - for (Contact *contact in contacts) { - for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) { - NSString *phoneNumberE164 = phoneNumber.toE164; + __block ContactsMaps *contactsMaps; + DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) { + NSString *_Nullable localNumber = [self.tsAccountManager localNumberWithTransaction:transaction]; + contactsMaps = [ContactsMaps buildWithContacts:contacts localNumber:localNumber]; + [self.contactsState setContactsMaps:contactsMaps localNumber:localNumber transaction:transaction]; + }); - // Ignore any system contact records for the local contact. - // For the local user we never want to show the avatar / - // name that you have entered for yourself in your system - // contacts. Instead, we always want to display your profile - // name and avatar. - BOOL isLocalContact = [phoneNumberE164 isEqualToString:TSAccountManager.localNumber]; - if (isLocalContact) { - [allContacts removeObject:contact]; - } else if (phoneNumberE164.length > 0) { - allContactsMap[phoneNumberE164] = contact; - } - } + if (SSKDebugFlags.internalLogging) { + OWSLogInfo(@"Updating contacts: %lu, phoneNumberToContactMap: %lu", + (unsigned long)contactsMaps.uniqueIdToContactMap.count, + (unsigned long)contactsMaps.phoneNumberToContactMap.count); } - NSArray *sortedContacts = [allContacts - sortedArrayUsingComparator:[Contact comparatorSortingNamesByFirstThenLast:self.shouldSortByGivenName]]; + [self.cnContactCache removeAllObjects]; + + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:OWSContactsManagerContactsDidChangeNotification + object:nil]; - dispatch_async(dispatch_get_main_queue(), ^{ - if (SSKDebugFlags.internalLogging) { - OWSLogInfo(@"sortedContacts: %lu, allContactsMap: %lu", - (unsigned long)sortedContacts.count, - (unsigned long)allContactsMap.count); + [self intersectContacts:contactsMaps.allContacts + isUserRequested:isUserRequested + completion:^(NSError *_Nullable error) { + if (error != nil) { + OWSFailDebug(@"Error: %@", error); + return; } - self.allContacts = sortedContacts; - self.allContactsMap = [allContactsMap copy]; - [self.cnContactCache removeAllObjects]; - - [[NSNotificationCenter defaultCenter] - postNotificationNameAsync:OWSContactsManagerContactsDidChangeNotification - object:nil]; - - [self intersectContacts:allContacts - isUserRequested:isUserRequested - completion:^(NSError *_Nullable error) { - if (error != nil) { - OWSFailDebug(@"Error: %@", error); - return; - } - [OWSContactsManager - buildSignalAccountsForContacts:sortedContacts - shouldClearStaleCache:shouldClearStaleCache - completion:^(NSArray *signalAccounts) { - [self updateSignalAccounts:signalAccounts - shouldSetHasLoadedContacts:didLoad]; - }]; - }]; - }); + [OWSContactsManager buildSignalAccountsForContacts:contactsMaps.allContacts + shouldClearStaleCache:shouldClearStaleCache + completion:^(NSArray *signalAccounts) { + [self updateSignalAccountsForSystemContactsFetch:signalAccounts + shouldSetHasLoadedSystemContacts:didLoad]; + }]; + }]; }); } -- (void)updateSignalAccounts:(NSArray *)signalAccounts - shouldSetHasLoadedContacts:(BOOL)shouldSetHasLoadedContacts +- (void)updateSignalAccountsForSystemContactsFetch:(NSArray *)newSignalAccounts + shouldSetHasLoadedSystemContacts:(BOOL)shouldSetHasLoadedSystemContacts { - OWSAssertIsOnMainThread(); - - BOOL hadLoadedContacts = self.hasLoadedContacts; - if (shouldSetHasLoadedContacts) { - _hasLoadedContacts = YES; + BOOL hadLoadedContacts = self.hasLoadedSystemContacts; + if (shouldSetHasLoadedSystemContacts) { + _hasLoadedSystemContacts = YES; } - if ([signalAccounts isEqual:self.signalAccounts]) { + __block NSArray *oldSortedSignalAccounts; + __block NSArray *newSortedSignalAccounts; + [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { + oldSortedSignalAccounts = [self sortedSignalAccountsWithTransaction:transaction]; + newSortedSignalAccounts = [self sortSignalAccounts:newSignalAccounts transaction:transaction]; + }]; + + if ([oldSortedSignalAccounts isEqual:newSortedSignalAccounts]) { OWSLogDebug(@"SignalAccounts unchanged."); self.isSetup = YES; - - if (hadLoadedContacts != self.hasLoadedContacts) { + + if (hadLoadedContacts != self.hasLoadedSystemContacts) { [[NSNotificationCenter defaultCenter] - postNotificationNameAsync:OWSContactsManagerSignalAccountsDidChangeNotification - object:nil]; + postNotificationNameAsync:OWSContactsManagerSignalAccountsDidChangeNotification + object:nil]; } - + return; } - NSMutableArray *allAddresses = [NSMutableArray new]; - for (SignalAccount *signalAccount in signalAccounts) { - [allAddresses addObject:signalAccount.recipientAddress]; + NSArray *sortedSignalAccounts = [self sortSignalAccountsWithSneakyTransaction:newSignalAccounts]; + [self.contactsState setSortedSignalAccounts:sortedSignalAccounts]; + + if (CurrentAppContext().isMainApp) { + // Ensure all system contacts are in the profile whitelist. + NSMutableArray *allAddresses = [NSMutableArray new]; + for (SignalAccount *signalAccount in newSignalAccounts) { + [allAddresses addObject:signalAccount.recipientAddress]; + } + [self.profileManagerImpl setContactAddresses:allAddresses]; } - self.signalAccounts = [self sortSignalAccountsWithSneakyTransaction:signalAccounts]; - - [self.profileManagerImpl setContactAddresses:allAddresses]; - - self.isSetup = YES; - [[NSNotificationCenter defaultCenter] postNotificationNameAsync:OWSContactsManagerSignalAccountsDidChangeNotification object:nil]; } -- (nullable NSString *)cachedContactNameForAddress:(SignalServiceAddress *)address -{ - SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForAddress:address]; - return [self cachedContactNameForAddress:address signalAccount:signalAccount]; -} - - (nullable NSString *)cachedContactNameForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction { @@ -570,30 +540,23 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan // if signalAccount is nil. phoneNumber = [self phoneNumberForAddress:address transaction:transaction]; } - return [self cachedContactNameForAddress:address signalAccount:signalAccount phoneNumber:phoneNumber]; -} - -- (nullable NSString *)cachedContactNameForAddress:(SignalServiceAddress *)address - signalAccount:(nullable SignalAccount *)signalAccount -{ - NSString *_Nullable phoneNumber = nil; - if (signalAccount == nil) { - // cachedContactNameForAddress only needs the phone number - // if signalAccount is nil. - phoneNumber = [self phoneNumberForAddress:address]; - } - return [self cachedContactNameForAddress:address signalAccount:signalAccount phoneNumber:phoneNumber]; + return [self cachedContactNameForAddress:address + signalAccount:signalAccount + phoneNumber:phoneNumber + transaction:transaction]; } - (nullable NSString *)cachedContactNameForAddress:(SignalServiceAddress *)address signalAccount:(nullable SignalAccount *)signalAccount phoneNumber:(nullable NSString *)phoneNumber + transaction:(SDSAnyReadTransaction *)transaction { OWSAssertDebug(address); if (!signalAccount) { // search system contacts for no-longer-registered signal users, for which there will be no SignalAccount - Contact *_Nullable nonSignalContact = self.allContactsMap[phoneNumber]; + Contact *_Nullable nonSignalContact = [self contactForPhoneNumber:phoneNumber + transaction:transaction]; if (!nonSignalContact) { return nil; } @@ -624,27 +587,19 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan phoneNumber = [self phoneNumberForAddress:address transaction:transaction]; } - return [self cachedContactNameComponentsForSignalAccount:signalAccount phoneNumber:phoneNumber]; -} - -- (nullable NSPersonNameComponents *)cachedContactNameComponentsForAddress:(SignalServiceAddress *)address -{ - SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForAddress:address]; - NSString *_Nullable phoneNumber = nil; - if (signalAccount == nil) { - // We only need the phone number if signalAccount is nil. - phoneNumber = [self phoneNumberForAddress:address]; - } - - return [self cachedContactNameComponentsForSignalAccount:signalAccount phoneNumber:phoneNumber]; + return [self cachedContactNameComponentsForSignalAccount:signalAccount + phoneNumber:phoneNumber + transaction:transaction]; } - (nullable NSPersonNameComponents *)cachedContactNameComponentsForSignalAccount:(nullable SignalAccount *)signalAccount phoneNumber:(nullable NSString *)phoneNumber + transaction:(SDSAnyReadTransaction *)transaction { if (!signalAccount) { // search system contacts for no-longer-registered signal users, for which there will be no SignalAccount - Contact *_Nullable nonSignalContact = self.allContactsMap[phoneNumber]; + Contact *_Nullable nonSignalContact = [self.contactsState contactForPhoneNumber:phoneNumber + transaction:transaction]; if (!nonSignalContact) { return nil; } @@ -688,20 +643,41 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan #pragma mark - Whisper User Management +- (BOOL)isSystemContactWithPhoneNumberWithSneakyTransaction:(NSString *)phoneNumber +{ + __block BOOL result; + [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { + result = [self isSystemContactWithPhoneNumber:phoneNumber transaction:transaction]; + }]; + return result; +} + - (BOOL)isSystemContactWithPhoneNumber:(NSString *)phoneNumber + transaction:(SDSAnyReadTransaction *)transaction { OWSAssertDebug(phoneNumber.length > 0); - return self.allContactsMap[phoneNumber] != nil; + return [self.contactsState contactForPhoneNumber:phoneNumber + transaction:transaction] != nil; +} + +- (BOOL)isSystemContactWithAddressWithSneakyTransaction:(SignalServiceAddress *)address +{ + __block BOOL result; + [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { + result = [self isSystemContactWithAddress:address transaction:transaction]; + }]; + return result; } - (BOOL)isSystemContactWithAddress:(SignalServiceAddress *)address + transaction:(SDSAnyReadTransaction *)transaction { - NSString *phoneNumber = address.phoneNumber; + NSString *_Nullable phoneNumber = address.phoneNumber; if (phoneNumber.length == 0) { return NO; } - return [self isSystemContactWithPhoneNumber:phoneNumber]; + return [self isSystemContactWithPhoneNumber:phoneNumber transaction:transaction]; } - (BOOL)isSystemContactWithSignalAccount:(SignalServiceAddress *)address @@ -719,11 +695,6 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return [self hasSignalAccountForAddress:address transaction:transaction]; } -- (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address -{ - return [self cachedContactNameForAddress:address].length > 0; -} - - (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction { @@ -772,11 +743,6 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan @"UNKNOWN_CONTACT_NAME", @"Displayed if for some reason we can't determine a contacts phone number *or* name"); } -- (nullable NSString *)nameFromSystemContactsForAddress:(SignalServiceAddress *)address -{ - return [self cachedContactNameForAddress:address]; -} - - (nullable NSString *)nameFromSystemContactsForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction { @@ -823,7 +789,8 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return self.unknownUserLabel; } - + +// TODO: Remove? - (NSString *)displayNameForAddress:(SignalServiceAddress *)address { OWSAssertDebug(address.isValid); @@ -842,13 +809,6 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return NSLocalizedString(@"UNKNOWN_USER", @"Label indicating an unknown user."); } -- (NSString *_Nonnull)displayNameForSignalAccount:(SignalAccount *)signalAccount -{ - OWSAssertDebug(signalAccount); - - return [self displayNameForAddress:signalAccount.recipientAddress]; -} - - (NSString *)shortDisplayNameForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction { @@ -870,22 +830,6 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return [OWSFormat formatNameComponentsShort:nameComponents]; } -- (nullable NSPersonNameComponents *)nameComponentsForAddress:(SignalServiceAddress *)address -{ - OWSAssertDebug(address.isValid); - - NSPersonNameComponents *_Nullable savedContactNameComponents = [self cachedContactNameComponentsForAddress:address]; - if (savedContactNameComponents) { - return savedContactNameComponents; - } - - __block NSPersonNameComponents *_Nullable profileNameComponents; - [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { - profileNameComponents = [self.profileManagerImpl nameComponentsForAddress:address transaction:transaction]; - }]; - return profileNameComponents; -} - - (nullable NSPersonNameComponents *)nameComponentsForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction { @@ -900,6 +844,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return [self.profileManagerImpl nameComponentsForAddress:address transaction:transaction]; } +// TODO: Remove? - (nullable SignalAccount *)fetchSignalAccountForAddress:(SignalServiceAddress *)address { OWSAssertDebug(address); @@ -939,6 +884,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return [self fetchSignalAccountForAddress:address transaction:transaction] != nil; } +// TODO: Remove? - (nullable NSData *)profileImageDataForAddressWithSneakyTransaction:(nullable SignalServiceAddress *)address { if (address == nil) { @@ -981,38 +927,6 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return contact.comparableNameLastFirst; } -- (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount -{ - NSString *_Nullable name = [self comparableNameForContact:signalAccount.contact]; - - if (name.length > 0) { - return name; - } - - NSString *_Nullable phoneNumber = signalAccount.recipientPhoneNumber; - if (phoneNumber != nil) { - Contact *_Nullable contact = self.allContactsMap[phoneNumber]; - NSString *_Nullable comparableContactName = [self comparableNameForContact:contact]; - if (comparableContactName.length > 0) { - return comparableContactName; - } - } - - __block NSPersonNameComponents *_Nullable nameComponents; - [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { - nameComponents = [self nameComponentsForAddress:signalAccount.recipientAddress transaction:transaction]; - }]; - - if (nameComponents != nil && nameComponents.givenName.length > 0 && nameComponents.familyName.length > 0) { - NSString *leftName = self.shouldSortByGivenName ? nameComponents.givenName : nameComponents.familyName; - NSString *rightName = self.shouldSortByGivenName ? nameComponents.familyName : nameComponents.givenName; - return [NSString stringWithFormat:@"%@\t%@", leftName, rightName]; - } - - // Fall back to non-contact display name. - return [self displayNameForSignalAccount:signalAccount]; -} - - (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount transaction:(SDSAnyReadTransaction *)transaction { @@ -1024,7 +938,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan NSString *_Nullable phoneNumber = signalAccount.recipientPhoneNumber; if (phoneNumber != nil) { - Contact *_Nullable contact = self.allContactsMap[phoneNumber]; + Contact *_Nullable contact = [self contactForPhoneNumber:phoneNumber transaction:transaction]; NSString *_Nullable comparableContactName = [self comparableNameForContact:contact]; if (comparableContactName.length > 0) { return comparableContactName; @@ -1044,6 +958,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return [self displayNameForAddress:signalAccount.recipientAddress transaction:transaction]; } +// TODO: Remove? - (BOOL)isKnownRegisteredUserWithSneakyTransaction:(SignalServiceAddress *)address { __block BOOL result; diff --git a/SignalMessaging/contacts/OWSContactsManager.swift b/SignalMessaging/contacts/OWSContactsManager.swift index c57945c232..6ba5daa219 100644 --- a/SignalMessaging/contacts/OWSContactsManager.swift +++ b/SignalMessaging/contacts/OWSContactsManager.swift @@ -3,6 +3,7 @@ // import Foundation +import SignalServiceKit // TODO: Should we use these caches in NSE? fileprivate extension OWSContactsManager { @@ -566,7 +567,7 @@ extension OWSContactsManager { } if let phoneNumber = self.phoneNumber(for: address, transaction: transaction), - let contact = self.allContactsMap[phoneNumber], + let contact = contactsState.contact(forPhoneNumber: phoneNumber, transaction: transaction), let cnContactId = contact.cnContactId, let avatarData = self.avatarData(forCNContactId: cnContactId), let validData = validateIfNecessary(avatarData) { @@ -884,3 +885,93 @@ public extension Array where Element == SignalAccount { } } } + +// MARK: - + +extension OWSContactsManager { + + @objc + public func unsortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] { + contactsState.unsortedSignalAccounts(transaction: transaction) + } + + @objc + public func unsortedSignalAccountsWithSneakyTransaction() -> [SignalAccount] { + databaseStorage.read { transaction in + unsortedSignalAccounts(transaction: transaction) + } + } + + // Order respects the systems contact sorting preference. + @objc + public func sortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] { + contactsState.sortedSignalAccounts(transaction: transaction) + } + + @objc + public func sortedSignalAccountsWithSneakyTransaction() -> [SignalAccount] { + databaseStorage.read { transaction in + sortedSignalAccounts(transaction: transaction) + } + } + + private static func removeLocalContact(contacts: [Contact], + transaction: SDSAnyReadTransaction) -> [Contact] { + let localNumber: String? = tsAccountManager.localNumber(with: transaction) + + var contactIdSet = Set() + return contacts.compactMap { contact in + // Skip local contacts. + func isLocalContact() -> Bool { + for phoneNumber in contact.parsedPhoneNumbers { + if phoneNumber.toE164() == localNumber { + return true + } + } + return false + } + guard !isLocalContact() else { + return nil + } + // De-deduplicate. + guard !contactIdSet.contains(contact.uniqueId) else { + return nil + } + contactIdSet.insert(contact.uniqueId) + return contact + } + } + + @objc + public func allUnsortedContacts(transaction: SDSAnyReadTransaction) -> [Contact] { + Self.removeLocalContact(contacts: contactsState.allContacts(transaction: transaction), + transaction: transaction) + } + + @objc + public func allSortedContacts(transaction: SDSAnyReadTransaction) -> [Contact] { + let contacts = (allUnsortedContacts(transaction: transaction) as NSArray) + let comparator = Contact.comparatorSortingNames(byFirstThenLast: self.shouldSortByGivenName) + return contacts.sortedArray(options: [], usingComparator: comparator) as! [Contact] + } + + @objc + public func comparableNameForSignalAccountWithSneakyTransaction(_ signalAccount: SignalAccount) -> String { + databaseStorage.read { transaction in + self.comparableName(for: signalAccount, transaction: transaction) + } + } + + @objc + public func displayName(forSignalAccount signalAccount: SignalAccount, + transaction: SDSAnyReadTransaction) -> String { + self.displayName(for: signalAccount.recipientAddress, transaction: transaction) + } + + @objc + public func displayNameForSignalAccountWithSneakyTransaction(_ signalAccount: SignalAccount) -> String { + databaseStorage.read { transaction in + self.displayName(forSignalAccount: signalAccount, transaction: transaction) + } + } +} diff --git a/SignalMessaging/contacts/OWSSyncManager.m b/SignalMessaging/contacts/OWSSyncManager.m index fbeba6afb0..7efbab4ec4 100644 --- a/SignalMessaging/contacts/OWSSyncManager.m +++ b/SignalMessaging/contacts/OWSSyncManager.m @@ -287,7 +287,8 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag - (AnyPromise *)syncAllContacts { - return [self syncContactsForSignalAccounts:self.contactsManager.signalAccounts + NSArray *allSignalAccounts = [self.contactsManagerImpl unsortedSignalAccountsWithSneakyTransaction]; + return [self syncContactsForSignalAccounts:allSignalAccounts skipIfRedundant:NO debounce:NO isDurableSend:NO]; @@ -301,7 +302,9 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag - (void)sendSyncContactsMessageIfNecessary { OWSAssertDebug(self.tsAccountManager.isRegisteredPrimaryDevice); - [self syncContactsForSignalAccounts:self.contactsManager.signalAccounts + + NSArray *allSignalAccounts = [self.contactsManagerImpl unsortedSignalAccountsWithSneakyTransaction]; + [self syncContactsForSignalAccounts:allSignalAccounts skipIfRedundant:YES debounce:YES isDurableSend:NO]; diff --git a/SignalServiceKit/src/Contacts/Contact.h b/SignalServiceKit/src/Contacts/Contact.h index ae158d7b5b..de45c3fbce 100644 --- a/SignalServiceKit/src/Contacts/Contact.h +++ b/SignalServiceKit/src/Contacts/Contact.h @@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) NSString *cnContactId; @property (nonatomic, readonly) BOOL isFromContactSync; +// This property should only be accessed from Swift. +@property (nonatomic, readonly) NSDictionary *phoneNumberNameMap; + #if TARGET_OS_IOS - (instancetype)initWithSystemContact:(CNContact *)cnContact; diff --git a/SignalServiceKit/src/Contacts/Contact.m b/SignalServiceKit/src/Contacts/Contact.m index d7bda3549b..693cb90e0d 100644 --- a/SignalServiceKit/src/Contacts/Contact.m +++ b/SignalServiceKit/src/Contacts/Contact.m @@ -14,14 +14,6 @@ NS_ASSUME_NONNULL_BEGIN -@interface Contact () - -@property (nonatomic, readonly) NSDictionary *phoneNumberNameMap; - -@end - -#pragma mark - - @implementation Contact @synthesize comparableNameFirstLast = _comparableNameFirstLast; diff --git a/SignalServiceKit/src/Contacts/Contact.swift b/SignalServiceKit/src/Contacts/Contact.swift new file mode 100644 index 0000000000..e85b850e76 --- /dev/null +++ b/SignalServiceKit/src/Contacts/Contact.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// + +import Foundation + +extension Contact { + public func isEqualForCache(_ other: Contact) -> Bool { + guard self.firstName == other.firstName, + self.lastName == other.lastName, + self.nickname == other.nickname, + self.fullName == other.fullName, + self.comparableNameFirstLast == other.comparableNameFirstLast, + self.comparableNameLastFirst == other.comparableNameLastFirst, + self.uniqueId == other.uniqueId, + self.cnContactId == other.cnContactId, + self.isFromContactSync == other.isFromContactSync, + self.phoneNumberNameMap == other.phoneNumberNameMap else { + return false + } + + // Ignore ordering of these properties. + guard Set(self.userTextPhoneNumbers) == Set(other.userTextPhoneNumbers), + Set(self.emails) == Set(other.emails) else { + return false + } + + // Ignore ordering of this property, + // Don't rely on equality of PhoneNumber. + let parsedPhoneNumbersSelf = Set(self.parsedPhoneNumbers.compactMap { $0.toE164() }) + let parsedPhoneNumbersOther = Set(other.parsedPhoneNumbers.compactMap { $0.toE164() }) + guard parsedPhoneNumbersSelf == parsedPhoneNumbersOther else { + return false + } + + return true + } +} diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.h b/SignalServiceKit/src/Messages/Interactions/OWSContact.h index 1316417708..88a0abf182 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.h +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.h @@ -155,7 +155,7 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value); - (NSArray *)systemContactsWithSignalAccountPhoneNumbers; - (NSArray *)systemContactsWithSignalAccountPhoneNumbersWithTransaction: (SDSAnyReadTransaction *)transaction; -- (NSArray *)systemContactPhoneNumbers; +- (NSArray *)systemContactPhoneNumbersWithTransaction:(SDSAnyReadTransaction *)transaction; - (NSArray *)e164PhoneNumbers NS_SWIFT_NAME(e164PhoneNumbers()); @end diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.m b/SignalServiceKit/src/Messages/Interactions/OWSContact.m index b5f4a6046f..ff7b1a1963 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.m @@ -561,12 +561,13 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value) }]]; } -- (NSArray *)systemContactPhoneNumbers +- (NSArray *)systemContactPhoneNumbersWithTransaction:(SDSAnyReadTransaction *)transaction { return [self.e164PhoneNumbers filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable recipientId, NSDictionary *_Nullable bindings) { - return [OWSContact.contactsManager isSystemContactWithPhoneNumber:recipientId]; + return [OWSContact.contactsManager isSystemContactWithPhoneNumber:recipientId + transaction:transaction]; }]]; } diff --git a/SignalServiceKit/src/Messages/MessageSender.m b/SignalServiceKit/src/Messages/MessageSender.m index dff5509cf4..1d011271e1 100644 --- a/SignalServiceKit/src/Messages/MessageSender.m +++ b/SignalServiceKit/src/Messages/MessageSender.m @@ -616,7 +616,7 @@ NSString *const MessageSenderSpamChallengeResolvedException = @"SpamChallengeRes }); OWSLogDebug(@"built message: %@ plainTextData.length: %lu", [message class], (unsigned long)plaintext.length); - // 2. gather "ud sending access" using a single write transaction. + // 2. Gather "ud sending access". NSMutableDictionary *sendingAccessMap = [NSMutableDictionary new]; if (senderCertificates != nil) { for (SignalServiceAddress *address in addresses) { diff --git a/SignalServiceKit/src/Messages/UD/OWSUDManager.swift b/SignalServiceKit/src/Messages/UD/OWSUDManager.swift index ac6f3cbb1e..0ed20a5218 100644 --- a/SignalServiceKit/src/Messages/UD/OWSUDManager.swift +++ b/SignalServiceKit/src/Messages/UD/OWSUDManager.swift @@ -493,7 +493,10 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { case .everybody: senderCertificate = senderCertificates.defaultCert case .contactsOnly: - if Self.contactsManager.isSystemContact(address: address) { + let isSystemContact = databaseStorage.read { transaction in + Self.contactsManager.isSystemContact(address: address, transaction: transaction) + } + if isSystemContact { senderCertificate = senderCertificates.defaultCert } else { senderCertificate = senderCertificates.uuidOnlyCert diff --git a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h index 71088eecd0..297be4245a 100644 --- a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h +++ b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h @@ -25,7 +25,6 @@ NS_ASSUME_NONNULL_BEGIN /// - The address' UUID - (NSString *)displayNameForAddress:(SignalServiceAddress *)address; - (NSString *)displayNameForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; -- (NSString *)displayNameForSignalAccount:(SignalAccount *)signalAccount; /// Returns the user's nickname / first name, if supported by the name's locale. /// If we don't know the user's name components, falls back to displayNameForAddress: @@ -41,7 +40,6 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)shortDisplayNameForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; -- (nullable NSPersonNameComponents *)nameComponentsForAddress:(SignalServiceAddress *)address; - (nullable NSPersonNameComponents *)nameComponentsForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; @@ -49,16 +47,18 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)displayNameForThreadWithSneakyTransaction:(TSThread *)thread NS_SWIFT_NAME(displayNameWithSneakyTransaction(thread:)); -- (NSArray *)signalAccounts; - -- (BOOL)isSystemContactWithPhoneNumber:(NSString *)phoneNumber NS_SWIFT_NAME(isSystemContact(phoneNumber:)); -- (BOOL)isSystemContactWithAddress:(SignalServiceAddress *)address NS_SWIFT_NAME(isSystemContact(address:)); +- (BOOL)isSystemContactWithPhoneNumberWithSneakyTransaction:(NSString *)phoneNumber NS_SWIFT_NAME(isSystemContactWithSneakyTransaction(phoneNumber:)); +- (BOOL)isSystemContactWithPhoneNumber:(NSString *)phoneNumber + transaction:(SDSAnyReadTransaction *)transaction NS_SWIFT_NAME(isSystemContact(phoneNumber:transaction:)); +- (BOOL)isSystemContactWithAddressWithSneakyTransaction:(SignalServiceAddress *)address +NS_SWIFT_NAME(isSystemContactWithSneakyTransaction(address:)); +- (BOOL)isSystemContactWithAddress:(SignalServiceAddress *)address + transaction:(SDSAnyReadTransaction *)transaction NS_SWIFT_NAME(isSystemContact(address:transaction:)); - (BOOL)isSystemContactWithSignalAccount:(SignalServiceAddress *)address NS_SWIFT_NAME(isSystemContactWithSignalAccount(_:)); - (BOOL)isSystemContactWithSignalAccount:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction NS_SWIFT_NAME(isSystemContactWithSignalAccount(_:transaction:)); -- (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address; - (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; diff --git a/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift b/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift index b70cb4a4cf..8afd1a1b4e 100644 --- a/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift +++ b/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift @@ -303,6 +303,8 @@ public class SDSKeyValueStore: NSObject { write(codingValue, forKey: key, transaction: transaction) } + // MARK: - + @objc public func removeValue(forKey key: String, transaction: SDSAnyWriteTransaction) { write(nil, forKey: key, transaction: transaction) diff --git a/SignalServiceKit/src/TestUtils/FakeContactsManager.swift b/SignalServiceKit/src/TestUtils/FakeContactsManager.swift index 6bf79774ea..de823fbf1e 100644 --- a/SignalServiceKit/src/TestUtils/FakeContactsManager.swift +++ b/SignalServiceKit/src/TestUtils/FakeContactsManager.swift @@ -26,10 +26,6 @@ public class FakeContactsManager: NSObject, ContactsManagerProtocol { return "Short fake name" } - public func nameComponents(for address: SignalServiceAddress) -> PersonNameComponents? { - return PersonNameComponents() - } - public func nameComponents(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> PersonNameComponents? { return PersonNameComponents() } @@ -42,11 +38,19 @@ public class FakeContactsManager: NSObject, ContactsManagerProtocol { return [] } - public func isSystemContact(phoneNumber: String) -> Bool { + public func isSystemContactWithSneakyTransaction(phoneNumber: String) -> Bool { return true } - public func isSystemContact(address: SignalServiceAddress) -> Bool { + public func isSystemContact(phoneNumber: String, transaction: SDSAnyReadTransaction) -> Bool { + return true + } + + public func isSystemContactWithSneakyTransaction(address: SignalServiceAddress) -> Bool { + return true + } + + public func isSystemContact(address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> Bool { return true } @@ -58,10 +62,6 @@ public class FakeContactsManager: NSObject, ContactsManagerProtocol { return true } - public func hasNameInSystemContacts(for address: SignalServiceAddress) -> Bool { - return true - } - public func hasNameInSystemContacts(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> Bool { return true } diff --git a/SignalServiceKit/src/Util/LRUCache.swift b/SignalServiceKit/src/Util/LRUCache.swift index 5918442df7..307a585e1b 100644 --- a/SignalServiceKit/src/Util/LRUCache.swift +++ b/SignalServiceKit/src/Util/LRUCache.swift @@ -117,7 +117,7 @@ public class LRUCache { return } guard maxSize > 0 else { - Logger.warn("Using disabled cache.") + Logger.verbose("Using disabled cache.") return } cache.setObject(value as AnyObject, forKey: key as AnyObject) diff --git a/SignalServiceKit/src/Util/LocalDevice.swift b/SignalServiceKit/src/Util/LocalDevice.swift index c23d627e86..841e546122 100644 --- a/SignalServiceKit/src/Util/LocalDevice.swift +++ b/SignalServiceKit/src/Util/LocalDevice.swift @@ -19,8 +19,7 @@ public class LocalDevice: NSObject { ProcessInfo.processInfo.activeProcessorCount } - @objc - public static var memoryUsage: String { + public static var memoryUsageUInt64: UInt64? { let vmInfoExpectedSize = MemoryLayout.size / MemoryLayout.size var vmInfo = task_vm_info_data_t() var vmInfoSize = mach_msg_type_number_t(vmInfoExpectedSize) @@ -37,9 +36,17 @@ public class LocalDevice: NSObject { guard kern == KERN_SUCCESS else { let errorString = String(cString: mach_error_string(kern), encoding: .ascii) ?? "Unknown error" owsFailDebug(errorString) - return "Unknown" + return nil } - return "\(vmInfo.phys_footprint)" + return vmInfo.phys_footprint + } + + @objc + public static var memoryUsage: String { + guard let memoryUsageUInt64 = self.memoryUsageUInt64 else { + return "Unknown" + } + return "\(memoryUsageUInt64)" } } diff --git a/SignalServiceKit/src/groups/GroupManager.swift b/SignalServiceKit/src/groups/GroupManager.swift index b780e34c7c..f6eafbb9ba 100644 --- a/SignalServiceKit/src/groups/GroupManager.swift +++ b/SignalServiceKit/src/groups/GroupManager.swift @@ -2352,7 +2352,8 @@ public class GroupManager: NSObject { } let isLocalAddress = groupUpdateSourceAddress.isLocalAddress - let isSystemContact = contactsManager.isSystemContact(address: groupUpdateSourceAddress) + let isSystemContact = contactsManager.isSystemContact(address: groupUpdateSourceAddress, + transaction: transaction) let isUserInProfileWhitelist = profileManager.isUser(inProfileWhitelist: groupUpdateSourceAddress, transaction: transaction) let shouldAddToWhitelist = (isLocalAddress || isSystemContact || isUserInProfileWhitelist) diff --git a/SignalServiceKit/tests/Messages/OWSUDManagerTest.swift b/SignalServiceKit/tests/Messages/OWSUDManagerTest.swift index 754fee754b..52c5144bb6 100644 --- a/SignalServiceKit/tests/Messages/OWSUDManagerTest.swift +++ b/SignalServiceKit/tests/Messages/OWSUDManagerTest.swift @@ -237,9 +237,13 @@ class OWSUDManagerTest: SSKBaseTestSwift { } let completed = self.expectation(description: "completed") - udManagerImpl.ensureSenderCertificates(certificateExpirationPolicy: .strict).done { senderCertificates in + firstly { + udManagerImpl.ensureSenderCertificates(certificateExpirationPolicy: .strict) + }.done { senderCertificates in do { - let sendingAccess = self.udManagerImpl.udSendingAccess(forAddress: bobRecipientAddress, requireSyncAccess: false, senderCertificates: senderCertificates)! + let sendingAccess = self.udManagerImpl.udSendingAccess(forAddress: bobRecipientAddress, + requireSyncAccess: false, + senderCertificates: senderCertificates)! XCTAssertEqual(.unknown, sendingAccess.udAccess.udAccessMode) XCTAssertFalse(sendingAccess.udAccess.isRandomKey) XCTAssertEqual(sendingAccess.senderCertificate.serialize(), diff --git a/SignalUI/Utils/BlockListUIUtils.m b/SignalUI/Utils/BlockListUIUtils.m index fd7818316d..875a26c185 100644 --- a/SignalUI/Utils/BlockListUIUtils.m +++ b/SignalUI/Utils/BlockListUIUtils.m @@ -54,7 +54,7 @@ typedef void (^BlockAlertCompletionBlock)(ActionSheetAction *action); fromViewController:(UIViewController *)fromViewController completionBlock:(nullable BlockActionCompletionBlock)completionBlock { - NSString *displayName = [self.contactsManager displayNameForSignalAccount:signalAccount]; + NSString *displayName = [self.contactsManagerImpl displayNameForSignalAccountWithSneakyTransaction:signalAccount]; [self showBlockAddressesActionSheet:@[ signalAccount.recipientAddress ] displayName:displayName fromViewController:fromViewController @@ -289,7 +289,7 @@ typedef void (^BlockAlertCompletionBlock)(ActionSheetAction *action); fromViewController:(UIViewController *)fromViewController completionBlock:(nullable BlockActionCompletionBlock)completionBlock { - NSString *displayName = [self.contactsManager displayNameForSignalAccount:signalAccount]; + NSString *displayName = [self.contactsManagerImpl displayNameForSignalAccountWithSneakyTransaction:signalAccount]; [self showUnblockAddressesActionSheet:@[ signalAccount.recipientAddress ] displayName:displayName fromViewController:fromViewController diff --git a/SignalUI/Utils/FullTextSearcher.swift b/SignalUI/Utils/FullTextSearcher.swift index 6cd54679e4..87e72ef461 100644 --- a/SignalUI/Utils/FullTextSearcher.swift +++ b/SignalUI/Utils/FullTextSearcher.swift @@ -421,7 +421,7 @@ public class FullTextSearcher: NSObject { if address.isLocalAddress { return omitLocalUser } - if self.contactsManager.isSystemContact(address: address) { + if self.contactsManager.isSystemContact(address: address, transaction: transaction) { return false } guard let thread = TSContactThread.getWithContactAddress(address, transaction: transaction) else { diff --git a/SignalUI/ViewModels/ContactShareViewModel.swift b/SignalUI/ViewModels/ContactShareViewModel.swift index 5ea1b41ae4..f9ae3909a8 100644 --- a/SignalUI/ViewModels/ContactShareViewModel.swift +++ b/SignalUI/ViewModels/ContactShareViewModel.swift @@ -124,8 +124,8 @@ public class ContactShareViewModel: NSObject { } @objc - public func systemContactPhoneNumbers() -> [String] { - return dbRecord.systemContactPhoneNumbers() + public func systemContactPhoneNumbers(transaction: SDSAnyReadTransaction) -> [String] { + return dbRecord.systemContactPhoneNumbers(with: transaction) } @objc diff --git a/SignalUI/Views/ContactsViewHelper.m b/SignalUI/Views/ContactsViewHelper.m index b1131bfa52..674c9232be 100644 --- a/SignalUI/Views/ContactsViewHelper.m +++ b/SignalUI/Views/ContactsViewHelper.m @@ -8,6 +8,7 @@ #import #import #import +#import #import #import #import @@ -217,7 +218,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssertDebug(!CurrentAppContext().isNSE); - return self.contactsManagerImpl.hasLoadedContacts; + return self.contactsManagerImpl.hasLoadedSystemContacts; } - (void)updateContacts @@ -227,24 +228,27 @@ NS_ASSUME_NONNULL_BEGIN NSMutableDictionary *phoneNumberSignalAccountMap = [NSMutableDictionary new]; NSMutableDictionary *uuidSignalAccountMap = [NSMutableDictionary new]; - NSMutableArray *signalAccounts = [NSMutableArray new]; - - NSMutableArray *accountsToProcess = [self.contactsManager.signalAccounts mutableCopy]; + __block NSArray *allSignalAccounts; __block NSArray *whitelistedAddresses; + __block ContactsMaps *contactsMaps; + [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { + allSignalAccounts = [self.contactsManagerImpl unsortedSignalAccountsWithTransaction:transaction]; whitelistedAddresses = [self.profileManagerImpl allWhitelistedRegisteredAddressesWithTransaction:transaction]; + contactsMaps = [self.contactsManagerImpl contactsMapsWithTransaction:transaction]; }]; + NSMutableArray *accountsToProcess = [allSignalAccounts mutableCopy]; for (SignalServiceAddress *address in whitelistedAddresses) { - if ([self.contactsManager isSystemContactWithAddress:address]) { + if ([contactsMaps isSystemContactWithAddress:address]) { continue; } - [accountsToProcess addObject:[[SignalAccount alloc] initWithSignalServiceAddress:address]]; } NSMutableSet *addressSet = [NSMutableSet new]; + NSMutableArray *signalAccounts = [NSMutableArray new]; for (SignalAccount *signalAccount in accountsToProcess) { if ([addressSet containsObject:signalAccount.recipientAddress]) { OWSLogVerbose(@"Ignoring duplicate: %@", signalAccount.recipientAddress); @@ -361,18 +365,18 @@ NS_ASSUME_NONNULL_BEGIN NSMutableSet *nonSignalContactSet = [NSMutableSet new]; __block NSArray *nonSignalContacts; - [self.databaseStorage - asyncReadWithBlock:^(SDSAnyReadTransaction *transaction) { - for (Contact *contact in self.contactsManagerImpl.allContactsMap.allValues) { - NSArray *signalRecipients = [contact signalRecipientsWithTransaction:transaction]; - if (signalRecipients.count < 1) { - [nonSignalContactSet addObject:contact]; - } + [self.databaseStorage asyncReadWithBlock:^(SDSAnyReadTransaction *transaction) { + NSArray *contacts = [self.contactsManagerImpl allSortedContactsWithTransaction:transaction]; + for (Contact *contact in contacts) { + NSArray *signalRecipients = [contact signalRecipientsWithTransaction:transaction]; + if (signalRecipients.count < 1) { + [nonSignalContactSet addObject:contact]; } - nonSignalContacts = [nonSignalContactSet.allObjects sortedArrayUsingComparator:^NSComparisonResult( - Contact *_Nonnull left, Contact *_Nonnull right) { return [left.fullName compare:right.fullName]; }]; } - completion:^{ self.nonSignalContacts = nonSignalContacts; }]; + nonSignalContacts = [nonSignalContactSet.allObjects sortedArrayUsingComparator:^NSComparisonResult( + Contact *_Nonnull left, Contact *_Nonnull right) { return [left.fullName compare:right.fullName]; }]; + } + completion:^{ self.nonSignalContacts = nonSignalContacts; }]; } - (nullable NSArray *)nonSignalContacts @@ -383,7 +387,8 @@ NS_ASSUME_NONNULL_BEGIN if (!_nonSignalContacts) { NSMutableSet *nonSignalContacts = [NSMutableSet new]; [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { - for (Contact *contact in self.contactsManagerImpl.allContactsMap.allValues) { + NSArray *contacts = [self.contactsManagerImpl allSortedContactsWithTransaction:transaction]; + for (Contact *contact in contacts) { NSArray *signalRecipients = [contact signalRecipientsWithTransaction:transaction]; if (signalRecipients.count < 1) { [nonSignalContacts addObject:contact];