diff --git a/submodules/AccountContext/BUILD b/submodules/AccountContext/BUILD index fb2ccfc448..acd19b9ff3 100644 --- a/submodules/AccountContext/BUILD +++ b/submodules/AccountContext/BUILD @@ -25,6 +25,8 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramCore/FlatBuffers", + "//submodules/TelegramCore/FlatSerialization", ], visibility = [ "//visibility:public", diff --git a/submodules/AccountContext/Sources/DeviceContactData.swift b/submodules/AccountContext/Sources/DeviceContactData.swift index cc223ba802..555533f8c8 100644 --- a/submodules/AccountContext/Sources/DeviceContactData.swift +++ b/submodules/AccountContext/Sources/DeviceContactData.swift @@ -1,6 +1,8 @@ import Foundation import Contacts import TelegramCore +import FlatBuffers +import FlatSerialization public final class DeviceContactPhoneNumberData: Equatable { public let label: String @@ -11,6 +13,22 @@ public final class DeviceContactPhoneNumberData: Equatable { self.value = value } + init(flatBuffersObject: TelegramCore_DeviceContactPhoneNumberData) { + self.label = flatBuffersObject.label + self.value = flatBuffersObject.value + } + + public func encodeToFlatBuffers(builder: inout FlatBufferBuilder) -> Offset { + let labelOffset = builder.create(string: self.label) + let valueOffset = builder.create(string: self.value) + + return TelegramCore_DeviceContactPhoneNumberData.createDeviceContactPhoneNumberData( + &builder, + labelOffset: labelOffset, + valueOffset: valueOffset + ) + } + public static func == (lhs: DeviceContactPhoneNumberData, rhs: DeviceContactPhoneNumberData) -> Bool { if lhs.label != rhs.label { return false @@ -216,6 +234,38 @@ public final class DeviceContactBasicData: Equatable { self.phoneNumbers = phoneNumbers } + public init(flatBuffersObject: TelegramCore_StoredDeviceContactData) { + self.firstName = flatBuffersObject.firstName + self.lastName = flatBuffersObject.lastName + + if flatBuffersObject.phoneNumbersCount == 1 { + self.phoneNumbers = [ + DeviceContactPhoneNumberData(flatBuffersObject: flatBuffersObject.phoneNumbers(at: 0)!) + ] + } else { + var phoneNumbers: [DeviceContactPhoneNumberData] = [] + for i in 0 ..< flatBuffersObject.phoneNumbersCount { + phoneNumbers.append(DeviceContactPhoneNumberData(flatBuffersObject: flatBuffersObject.phoneNumbers(at: i)!)) + } + self.phoneNumbers = phoneNumbers + } + } + + public func encodeToFlatBuffers(builder: inout FlatBufferBuilder) -> Offset { + let phoneNumberOffsets = self.phoneNumbers.map { $0.encodeToFlatBuffers(builder: &builder) } + let phoneNumberOffset = builder.createVector(ofOffsets: phoneNumberOffsets, len: phoneNumberOffsets.count) + + let firstNameOffset = builder.create(string: self.firstName) + let lastNameOffset = builder.create(string: self.lastName) + + return TelegramCore_StoredDeviceContactData.createStoredDeviceContactData( + &builder, + firstNameOffset: firstNameOffset, + lastNameOffset: lastNameOffset, + phoneNumbersVectorOffset: phoneNumberOffset + ) + } + public static func ==(lhs: DeviceContactBasicData, rhs: DeviceContactBasicData) -> Bool { if lhs.firstName != rhs.firstName { return false @@ -230,6 +280,86 @@ public final class DeviceContactBasicData: Equatable { } } +public final class DeviceContactDataState: Codable { + private enum CodingKeys: String, CodingKey { + case contactsData + case contactsKeys + case telegramReferencesKeys + case telegramReferencesValues + case stateToken + } + + public let contacts: [String: DeviceContactBasicData] + public let telegramReferences: [EnginePeer.Id: String] + public let stateToken: Data? + + public init(contacts: [String: DeviceContactBasicData], telegramReferences: [EnginePeer.Id: String], stateToken: Data?) { + self.contacts = contacts + self.telegramReferences = telegramReferences + self.stateToken = stateToken + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let contactsData = try container.decode([Data].self, forKey: .contactsData).map { data in + var byteBuffer = ByteBuffer(data: data) + let deserializedValue = FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_StoredDeviceContactData + let parsedValue = DeviceContactBasicData(flatBuffersObject: deserializedValue) + return parsedValue + } + let contactsKeys = try container.decode([String].self, forKey: .contactsKeys) + + var contacts: [String: DeviceContactBasicData] = [:] + for i in 0 ..< min(contactsData.count, contactsKeys.count) { + contacts[contactsKeys[i]] = contactsData[i] + } + self.contacts = contacts + + let telegramReferencesKeys = try container.decode([Int64].self, forKey: .telegramReferencesKeys).map { value in + return EnginePeer.Id(value) + } + let telegramReferencesValues = try container.decode([String].self, forKey: .telegramReferencesValues) + + var telegramReferences: [EnginePeer.Id: String] = [:] + for i in 0 ..< min(telegramReferencesValues.count, telegramReferencesKeys.count) { + telegramReferences[telegramReferencesKeys[i]] = telegramReferencesValues[i] + } + self.telegramReferences = telegramReferences + + self.stateToken = try container.decodeIfPresent(Data.self, forKey: .stateToken) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + var contactsData: [Data] = [] + var contactsKeys: [String] = [] + for (key, contact) in self.contacts { + var builder = FlatBufferBuilder(initialSize: 1024) + let offset = contact.encodeToFlatBuffers(builder: &builder) + builder.finish(offset: offset) + contactsData.append(builder.data) + contactsKeys.append(key) + } + + var telegramReferencesKeys: [Int64] = [] + var telegramReferencesValues: [String] = [] + for (key, value) in self.telegramReferences { + telegramReferencesKeys.append(key.toInt64()) + telegramReferencesValues.append(value) + } + + try container.encode(contactsKeys, forKey: .contactsKeys) + try container.encode(contactsData, forKey: .contactsData) + + try container.encode(telegramReferencesKeys, forKey: .telegramReferencesKeys) + try container.encode(telegramReferencesValues, forKey: .telegramReferencesValues) + + try container.encodeIfPresent(stateToken, forKey: .stateToken) + } +} + public final class DeviceContactBasicDataWithReference: Equatable { public let stableId: DeviceContactStableId public let basicData: DeviceContactBasicData @@ -327,17 +457,12 @@ public final class DeviceContactExtendedData: Equatable { public extension DeviceContactExtendedData { convenience init?(vcard: Data) { - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - guard let contact = (try? CNContactVCardSerialization.contacts(with: vcard))?.first else { - return nil - } - self.init(contact: contact) - } else { + guard let contact = (try? CNContactVCardSerialization.contacts(with: vcard))?.first else { return nil } + self.init(contact: contact) } - @available(iOSApplicationExtension 9.0, iOS 9.0, *) func asMutableCNContact() -> CNMutableContact { let contact = CNMutableContact() contact.givenName = self.basicData.firstName @@ -385,7 +510,6 @@ public extension DeviceContactExtendedData { return nil } - @available(iOSApplicationExtension 9.0, iOS 9.0, *) convenience init(contact: CNContact) { var phoneNumbers: [DeviceContactPhoneNumberData] = [] for number in contact.phoneNumbers { diff --git a/submodules/AccountContext/Sources/DeviceContactDataManager.swift b/submodules/AccountContext/Sources/DeviceContactDataManager.swift index 9074332109..18effedcbd 100644 --- a/submodules/AccountContext/Sources/DeviceContactDataManager.swift +++ b/submodules/AccountContext/Sources/DeviceContactDataManager.swift @@ -5,6 +5,8 @@ import SwiftSignalKit public typealias DeviceContactStableId = String +public var sharedDisableDeviceContactDataDiffing: Bool = false + public protocol DeviceContactDataManager: AnyObject { func personNameDisplayOrder() -> Signal func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> diff --git a/submodules/ContactsHelper/BUILD b/submodules/ContactsHelper/BUILD new file mode 100644 index 0000000000..a1c8c19905 --- /dev/null +++ b/submodules/ContactsHelper/BUILD @@ -0,0 +1,22 @@ + +objc_library( + name = "ContactsHelper", + enable_modules = True, + module_name = "ContactsHelper", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.h", + ], allow_empty = True), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ContactsHelper/PublicHeaders/ContactsHelper/ContactsHelper.h b/submodules/ContactsHelper/PublicHeaders/ContactsHelper/ContactsHelper.h new file mode 100755 index 0000000000..917c87f287 --- /dev/null +++ b/submodules/ContactsHelper/PublicHeaders/ContactsHelper/ContactsHelper.h @@ -0,0 +1,30 @@ +#ifndef ContactsHelper_h +#define ContactsHelper_h + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ContactsEnumerateChangeResult : NSObject + +@property (nonatomic, strong) NSData * _Nullable stateToken; + +- (instancetype)initWithStateToken:(NSData *)stateToken; + +@end + +@interface ContactsEnumerateResult : NSObject + +@property (nonatomic, strong) NSData * _Nullable stateToken; + +- (instancetype)initWithStateToken:(NSData *)stateToken; + +@end + +ContactsEnumerateChangeResult * _Nullable ContactsEnumerateChangeRequest(CNContactStore *store, CNChangeHistoryFetchRequest *fetchRequest, id visitor); +ContactsEnumerateResult * _Nullable ContactsEnumerateRequest(CNContactStore *store, CNContactFetchRequest *fetchRequest, void (^onContact)(CNContact *)); + +NS_ASSUME_NONNULL_END + +#endif /* ContactsHelper_h */ diff --git a/submodules/ContactsHelper/Sources/ContactsHelper.m b/submodules/ContactsHelper/Sources/ContactsHelper.m new file mode 100755 index 0000000000..68afbf4087 --- /dev/null +++ b/submodules/ContactsHelper/Sources/ContactsHelper.m @@ -0,0 +1,46 @@ +#import + +@implementation ContactsEnumerateChangeResult + +- (instancetype)initWithStateToken:(NSData *)stateToken { + self = [super init]; + if (self != nil) { + _stateToken = stateToken; + } + return self; +} + +@end + +@implementation ContactsEnumerateResult + +- (instancetype)initWithStateToken:(NSData *)stateToken { + self = [super init]; + if (self != nil) { + _stateToken = stateToken; + } + return self; +} + +@end + +ContactsEnumerateChangeResult * _Nullable ContactsEnumerateChangeRequest(CNContactStore *store, CNChangeHistoryFetchRequest *fetchRequest, id visitor) { + NSError *error = nil; + CNFetchResult *> *fetchResult = [store enumeratorForChangeHistoryFetchRequest:fetchRequest error:&error]; + + for (CNChangeHistoryEvent *event in fetchResult.value) { + [event acceptEventVisitor:visitor]; + } + + return [[ContactsEnumerateChangeResult alloc] initWithStateToken:fetchResult.currentHistoryToken]; +} + +ContactsEnumerateResult * _Nullable ContactsEnumerateRequest(CNContactStore *store, CNContactFetchRequest *fetchRequest, void (^onContact)(CNContact *)) { + NSError *error = nil; + CNFetchResult *> *fetchResult = [store enumeratorForContactFetchRequest:fetchRequest error:&error]; + for (CNContact *contact in fetchResult.value) { + onContact(contact); + } + + return [[ContactsEnumerateResult alloc] initWithStateToken:fetchResult.currentHistoryToken]; +} diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index 42d428fef0..8db10249eb 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -7,7 +7,6 @@ import SwiftSignalKit import Photos import CoreLocation import Contacts -import AddressBook import UserNotifications import CoreTelephony import TelegramPresentationData @@ -155,29 +154,17 @@ public final class DeviceAccess { } case .contacts: let status = Signal { subscriber in - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - switch CNContactStore.authorizationStatus(for: .contacts) { - case .notDetermined: - subscriber.putNext(.notDetermined) - case .authorized: - subscriber.putNext(.allowed) - case .limited: - subscriber.putNext(.limited) - default: - subscriber.putNext(.denied) - } - subscriber.putCompletion() - } else { - switch ABAddressBookGetAuthorizationStatus() { - case .notDetermined: - subscriber.putNext(.notDetermined) - case .authorized: - subscriber.putNext(.allowed) - default: - subscriber.putNext(.denied) - } - subscriber.putCompletion() + switch CNContactStore.authorizationStatus(for: .contacts) { + case .notDetermined: + subscriber.putNext(.notDetermined) + case .authorized: + subscriber.putNext(.allowed) + case .limited: + subscriber.putNext(.limited) + default: + subscriber.putNext(.denied) } + subscriber.putCompletion() return EmptyDisposable } return status @@ -531,47 +518,22 @@ public final class DeviceAccess { if let value = value { completion(value) } else { - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - switch CNContactStore.authorizationStatus(for: .contacts) { - case .notDetermined: - let store = CNContactStore() - store.requestAccess(for: .contacts, completionHandler: { authorized, _ in - self.contactsPromise.set(.single(authorized)) - completion(authorized) - }) - case .authorized: - self.contactsPromise.set(.single(true)) - completion(true) - case .limited: - self.contactsPromise.set(.single(true)) - completion(true) - default: - self.contactsPromise.set(.single(false)) - completion(false) - } - } else { - switch ABAddressBookGetAuthorizationStatus() { - case .notDetermined: - var error: Unmanaged? - let addressBook = ABAddressBookCreateWithOptions(nil, &error) - if let addressBook = addressBook?.takeUnretainedValue() { - ABAddressBookRequestAccessWithCompletion(addressBook, { authorized, _ in - Queue.mainQueue().async { - self.contactsPromise.set(.single(authorized)) - completion(authorized) - } - }) - } else { - self.contactsPromise.set(.single(false)) - completion(false) - } - case .authorized: - self.contactsPromise.set(.single(true)) - completion(true) - default: - self.contactsPromise.set(.single(false)) - completion(false) - } + switch CNContactStore.authorizationStatus(for: .contacts) { + case .notDetermined: + let store = CNContactStore() + store.requestAccess(for: .contacts, completionHandler: { authorized, _ in + self.contactsPromise.set(.single(authorized)) + completion(authorized) + }) + case .authorized: + self.contactsPromise.set(.single(true)) + completion(true) + case .limited: + self.contactsPromise.set(.single(true)) + completion(true) + default: + self.contactsPromise.set(.single(false)) + completion(false) } } }) diff --git a/submodules/TelegramCore/FlatSerialization/Models/StoredDeviceContactData.fbs b/submodules/TelegramCore/FlatSerialization/Models/StoredDeviceContactData.fbs new file mode 100644 index 0000000000..b89461b524 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/StoredDeviceContactData.fbs @@ -0,0 +1,14 @@ +namespace TelegramCore; + +table DeviceContactPhoneNumberData { + label:string (id: 0, required); + value:string (id: 1, required); +} + +table StoredDeviceContactData { + firstName:string (id: 0, required); + lastName:string (id: 1, required); + phoneNumbers:[DeviceContactPhoneNumberData] (id: 2); +} + +root_type StoredDeviceContactData; diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 78a7f1ddb6..d3450a6f61 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -584,6 +584,7 @@ private enum SharedDataKeyValues: Int32 { case countriesList = 7 case wallapersState = 8 case chatThemes = 10 + case deviceContacts = 11 } public struct SharedDataKeys { @@ -640,6 +641,12 @@ public struct SharedDataKeys { key.setInt32(0, value: SharedDataKeyValues.chatThemes.rawValue) return key }() + + public static let deviceContacts: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: SharedDataKeyValues.deviceContacts.rawValue) + return key + }() } public func applicationSpecificItemCacheCollectionId(_ value: Int8) -> Int8 { diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 1c35635c1b..17dc241a3d 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -4,7 +4,6 @@ import SwiftSignalKit import Postbox import TelegramCore import Contacts -import AddressBook import Display import TelegramUIPreferences import AppBundle @@ -201,19 +200,11 @@ private func currentDateTimeFormat() -> PresentationDateTimeFormat { } private func currentPersonNameSortOrder() -> PresentationPersonNameOrder { - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - switch CNContactsUserDefaults.shared().sortOrder { - case .givenName: - return .firstLast - default: - return .lastFirst - } - } else { - if ABPersonGetSortOrdering() == kABPersonSortByFirstName { + switch CNContactsUserDefaults.shared().sortOrder { + case .givenName: return .firstLast - } else { + default: return .lastFirst - } } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 648f58c933..ea0a63e21b 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -482,6 +482,7 @@ swift_library( "//submodules/TelegramUI/Components/ComposeTodoScreen", "//submodules/TelegramUI/Components/SuggestedPostApproveAlert", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", + "//submodules/ContactsHelper", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index bd22a5f980..8a961f0d5f 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -359,6 +359,10 @@ public final class AccountContextImpl: AccountContext { self.appConfigurationDisposable = (self._appConfiguration.get() |> deliverOnMainQueue).start(next: { value in let _ = currentAppConfiguration.swap(value) + + if let data = appConfiguration.data, data["ios_killswitch_contact_diffing"] != nil { + sharedDisableDeviceContactDataDiffing = true + } }) let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode diff --git a/submodules/TelegramUI/Sources/DeviceContactDataManager.swift b/submodules/TelegramUI/Sources/DeviceContactDataManager.swift index b3597c8669..576b6d6f64 100644 --- a/submodules/TelegramUI/Sources/DeviceContactDataManager.swift +++ b/submodules/TelegramUI/Sources/DeviceContactDataManager.swift @@ -7,6 +7,8 @@ import TelegramUIPreferences import DeviceAccess import AccountContext import PhoneNumberFormat +import ContactsHelper +import AccountContext private protocol DeviceContactDataContext { func personNameDisplayOrder() -> PresentationPersonNameOrder @@ -17,62 +19,222 @@ private protocol DeviceContactDataContext { func deleteContactWithAppSpecificReference(peerId: PeerId) } -@available(iOSApplicationExtension 9.0, iOS 9.0, *) private final class DeviceContactDataModernContext: DeviceContactDataContext { + let queue: Queue + let accountManager: AccountManager let store = CNContactStore() var updateHandle: NSObjectProtocol? var currentContacts: [DeviceContactStableId: DeviceContactBasicData] = [:] var currentAppSpecificReferences: [PeerId: DeviceContactBasicDataWithReference] = [:] - init(queue: Queue, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void, appSpecificReferencesUpdated: @escaping ([PeerId: DeviceContactBasicDataWithReference]) -> Void) { - let (contacts, references) = self.retrieveContacts() - self.currentContacts = contacts - self.currentAppSpecificReferences = references - updated(self.currentContacts) - appSpecificReferencesUpdated(self.currentAppSpecificReferences) - let handle = NotificationCenter.default.addObserver(forName: NSNotification.Name.CNContactStoreDidChange, object: nil, queue: nil, using: { [weak self] _ in - queue.async { - guard let strongSelf = self else { - return - } - let (contacts, references) = strongSelf.retrieveContacts() - if strongSelf.currentContacts != contacts { - strongSelf.currentContacts = contacts - updated(strongSelf.currentContacts) - } - if strongSelf.currentAppSpecificReferences != references { - strongSelf.currentAppSpecificReferences = references - appSpecificReferencesUpdated(strongSelf.currentAppSpecificReferences) - } + private var retrieveContactsDisposable: Disposable? + + init(queue: Queue, accountManager: AccountManager, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void, appSpecificReferencesUpdated: @escaping ([PeerId: DeviceContactBasicDataWithReference]) -> Void) { + self.queue = queue + self.accountManager = accountManager + + self.retrieveContactsDisposable?.dispose() + self.retrieveContactsDisposable = (self.retrieveContacts() + |> deliverOn(self.queue)).startStrict(next: { [weak self] contacts, references in + guard let self else { + return } + + self.currentContacts = contacts + self.currentAppSpecificReferences = references + updated(self.currentContacts) + appSpecificReferencesUpdated(self.currentAppSpecificReferences) + let handle = NotificationCenter.default.addObserver(forName: NSNotification.Name.CNContactStoreDidChange, object: nil, queue: nil, using: { [weak self] _ in + queue.async { + guard let self else { + return + } + self.retrieveContactsDisposable?.dispose() + self.retrieveContactsDisposable = (self.retrieveContacts() + |> deliverOn(self.queue)).startStrict(next: { [weak self] contacts, references in + guard let self else { + return + } + + if self.currentContacts != contacts { + self.currentContacts = contacts + updated(self.currentContacts) + } + if self.currentAppSpecificReferences != references { + self.currentAppSpecificReferences = references + appSpecificReferencesUpdated(self.currentAppSpecificReferences) + } + }) + } + }) + self.updateHandle = handle }) - self.updateHandle = handle } deinit { - if let updateHandle = updateHandle { + self.retrieveContactsDisposable?.dispose() + if let updateHandle = self.updateHandle { NotificationCenter.default.removeObserver(updateHandle) } } - private func retrieveContacts() -> ([DeviceContactStableId: DeviceContactBasicData], [PeerId: DeviceContactBasicDataWithReference]) { - let keysToFetch: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor, CNContactUrlAddressesKey as CNKeyDescriptor] - - let request = CNContactFetchRequest(keysToFetch: keysToFetch) - request.unifyResults = true - - var result: [DeviceContactStableId: DeviceContactBasicData] = [:] - var references: [PeerId: DeviceContactBasicDataWithReference] = [:] - let _ = try? self.store.enumerateContacts(with: request, usingBlock: { contact, _ in - let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact) - result[stableIdAndContact.0] = stableIdAndContact.1 - for address in contact.urlAddresses { - if address.label == "Telegram", let peerId = parseAppSpecificContactReference(address.value as String) { - references[peerId] = DeviceContactBasicDataWithReference(stableId: stableIdAndContact.0, basicData: stableIdAndContact.1) + private func retrieveContacts() -> Signal<([DeviceContactStableId: DeviceContactBasicData], [PeerId: DeviceContactBasicDataWithReference]), NoError> { + return self.accountManager.transaction { transaction -> DeviceContactDataState? in + return transaction.getSharedData(SharedDataKeys.deviceContacts)?.get(DeviceContactDataState.self) + } + |> deliverOn(self.queue) + |> map { currentData -> ([DeviceContactStableId: DeviceContactBasicData], [PeerId: DeviceContactBasicDataWithReference]) in + if let currentData, let stateToken = currentData.stateToken, !sharedDisableDeviceContactDataDiffing { + final class ChangeVisitor: NSObject, CNChangeHistoryEventVisitor { + let onDropEverything: () -> Void + let onAdd: (CNContact) -> Void + let onUpdate: (CNContact) -> Void + let onDelete: (String) -> Void + + init(onDropEverything: @escaping () -> Void, onAdd: @escaping (CNContact) -> Void, onUpdate: @escaping (CNContact) -> Void, onDelete: @escaping (String) -> Void) { + self.onDropEverything = onDropEverything + self.onAdd = onAdd + self.onUpdate = onUpdate + self.onDelete = onDelete + + super.init() + } + + func visit(_ event: CNChangeHistoryDropEverythingEvent) { + self.onDropEverything() + } + + func visit(_ event: CNChangeHistoryAddContactEvent) { + self.onAdd(event.contact) + } + + func visit(_ event: CNChangeHistoryUpdateContactEvent) { + self.onUpdate(event.contact) + } + + func visit(_ event: CNChangeHistoryDeleteContactEvent) { + self.onDelete(event.contactIdentifier) + } } + + let changeRequest = CNChangeHistoryFetchRequest() + changeRequest.startingToken = stateToken + changeRequest.additionalContactKeyDescriptors = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactUrlAddressesKey as CNKeyDescriptor + ] + + var contacts: [DeviceContactStableId: DeviceContactBasicData] = currentData.contacts + var telegramReferences: [PeerId: String] = currentData.telegramReferences + var reverseTelegramReferences: [String: Set] = [:] + for (peerId, id) in telegramReferences { + if reverseTelegramReferences[id] == nil { + reverseTelegramReferences[id] = Set() + } + reverseTelegramReferences[id]?.insert(peerId) + } + + let visitor = ChangeVisitor( + onDropEverything: { + contacts.removeAll() + telegramReferences.removeAll() + reverseTelegramReferences.removeAll() + }, + onAdd: { contact in + let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact) + contacts[stableIdAndContact.0] = stableIdAndContact.1 + for address in contact.urlAddresses { + if address.label == "Telegram", let peerId = parseAppSpecificContactReference(address.value as String) { + telegramReferences[peerId] = stableIdAndContact.0 + if reverseTelegramReferences[stableIdAndContact.0] == nil { + reverseTelegramReferences[stableIdAndContact.0] = Set() + } + reverseTelegramReferences[stableIdAndContact.0]?.insert(peerId) + } + } + }, + onUpdate: { contact in + let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact) + contacts[stableIdAndContact.0] = stableIdAndContact.1 + for address in contact.urlAddresses { + if address.label == "Telegram", let peerId = parseAppSpecificContactReference(address.value as String) { + telegramReferences[peerId] = stableIdAndContact.0 + telegramReferences[peerId] = stableIdAndContact.0 + if reverseTelegramReferences[stableIdAndContact.0] == nil { + reverseTelegramReferences[stableIdAndContact.0] = Set() + } + reverseTelegramReferences[stableIdAndContact.0]?.insert(peerId) + } + } + }, + onDelete: { contactId in + contacts.removeValue(forKey: contactId) + if let peerIds = reverseTelegramReferences[contactId] { + for peerId in peerIds { + telegramReferences.removeValue(forKey: peerId) + } + reverseTelegramReferences.removeValue(forKey: contactId) + } + } + ) + + let resultState = ContactsEnumerateChangeRequest(self.store, changeRequest, visitor) + + let _ = self.accountManager.transaction({ transaction -> Void in + transaction.updateSharedData(SharedDataKeys.deviceContacts, { _ in + return PreferencesEntry(DeviceContactDataState( + contacts: contacts, telegramReferences: telegramReferences, stateToken: resultState?.stateToken)) + }) + }).startStandalone() + + var references: [PeerId: DeviceContactBasicDataWithReference] = [:] + for (peerId, id) in telegramReferences { + if let basicData = contacts[id] { + references[peerId] = DeviceContactBasicDataWithReference(stableId: id, basicData: basicData) + } + } + + return (contacts, references) + } else { + let keysToFetch: [CNKeyDescriptor] = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactUrlAddressesKey as CNKeyDescriptor + ] + + let request = CNContactFetchRequest(keysToFetch: keysToFetch) + request.unifyResults = true + + var contacts: [String: DeviceContactBasicData] = [:] + var telegramReferences: [EnginePeer.Id: String] = [:] + let resultState = ContactsEnumerateRequest(self.store, request, { contact in + let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact) + contacts[stableIdAndContact.0] = stableIdAndContact.1 + for address in contact.urlAddresses { + if address.label == "Telegram", let peerId = parseAppSpecificContactReference(address.value as String) { + telegramReferences[peerId] = stableIdAndContact.0 + } + } + }) + + let _ = self.accountManager.transaction({ transaction -> Void in + transaction.updateSharedData(SharedDataKeys.deviceContacts, { _ in + return PreferencesEntry(DeviceContactDataState( + contacts: contacts, telegramReferences: telegramReferences, stateToken: resultState?.stateToken)) + }) + }).startStandalone() + + var references: [PeerId: DeviceContactBasicDataWithReference] = [:] + for (peerId, id) in telegramReferences { + if let basicData = contacts[id] { + references[peerId] = DeviceContactBasicDataWithReference(stableId: id, basicData: basicData) + } + } + + return (contacts, references) } - }) - return (result, references) + } } private static func parseContact(_ contact: CNContact) -> (DeviceContactStableId, DeviceContactBasicData) { @@ -323,6 +485,7 @@ private final class BasicDataForNormalizedNumberContext { private final class DeviceContactDataManagerPrivateImpl { private let queue: Queue + private let accountManager: AccountManager private var accessInitialized = false @@ -345,8 +508,9 @@ private final class DeviceContactDataManagerPrivateImpl { private let importableContactsSubscribers = Bag<([DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]) -> Void>() private let appSpecificReferencesSubscribers = Bag<([PeerId: DeviceContactBasicDataWithReference]) -> Void>() - init(queue: Queue) { + init(queue: Queue, accountManager: AccountManager) { self.queue = queue + self.accountManager = accountManager self.accessDisposable = (DeviceAccess.authorizationStatus(subject: .contacts) |> delay(2.0, queue: .mainQueue()) |> deliverOn(self.queue)).startStrict(next: { [weak self] authorizationStatus in @@ -355,7 +519,7 @@ private final class DeviceContactDataManagerPrivateImpl { } strongSelf.accessInitialized = true if authorizationStatus == .allowed { - let dataContext = DeviceContactDataModernContext(queue: strongSelf.queue, updated: { stableIdToBasicContactData in + let dataContext = DeviceContactDataModernContext(queue: strongSelf.queue, accountManager: strongSelf.accountManager, updated: { stableIdToBasicContactData in guard let strongSelf = self else { return } @@ -605,10 +769,10 @@ public final class DeviceContactDataManagerImpl: DeviceContactDataManager { private let queue = Queue() private let impl: QueueLocalObject - init() { + init(accountManager: AccountManager) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return DeviceContactDataManagerPrivateImpl(queue: queue) + return DeviceContactDataManagerPrivateImpl(queue: queue, accountManager: accountManager) }) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 9a99810855..7cf0e4a471 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -316,7 +316,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { if applicationBindings.isMainApp { self.locationManager = DeviceLocationManager(queue: Queue.mainQueue()) - self.contactDataManager = DeviceContactDataManagerImpl() + self.contactDataManager = DeviceContactDataManagerImpl(accountManager: accountManager) } else { self.locationManager = nil self.contactDataManager = nil