New address book implementation

(cherry picked from commit a16a3c393628a3e44a14b0c7196b24205f40c1e4)
This commit is contained in:
Isaac 2025-07-08 15:57:27 +04:00
parent 681ab8248e
commit 735b497a23
14 changed files with 497 additions and 128 deletions

View File

@ -25,6 +25,8 @@ swift_library(
"//submodules/TextFormat:TextFormat", "//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramCore/FlatBuffers",
"//submodules/TelegramCore/FlatSerialization",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1,6 +1,8 @@
import Foundation import Foundation
import Contacts import Contacts
import TelegramCore import TelegramCore
import FlatBuffers
import FlatSerialization
public final class DeviceContactPhoneNumberData: Equatable { public final class DeviceContactPhoneNumberData: Equatable {
public let label: String public let label: String
@ -11,6 +13,22 @@ public final class DeviceContactPhoneNumberData: Equatable {
self.value = value 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 { public static func == (lhs: DeviceContactPhoneNumberData, rhs: DeviceContactPhoneNumberData) -> Bool {
if lhs.label != rhs.label { if lhs.label != rhs.label {
return false return false
@ -216,6 +234,38 @@ public final class DeviceContactBasicData: Equatable {
self.phoneNumbers = phoneNumbers 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 { public static func ==(lhs: DeviceContactBasicData, rhs: DeviceContactBasicData) -> Bool {
if lhs.firstName != rhs.firstName { if lhs.firstName != rhs.firstName {
return false 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 final class DeviceContactBasicDataWithReference: Equatable {
public let stableId: DeviceContactStableId public let stableId: DeviceContactStableId
public let basicData: DeviceContactBasicData public let basicData: DeviceContactBasicData
@ -327,17 +457,12 @@ public final class DeviceContactExtendedData: Equatable {
public extension DeviceContactExtendedData { public extension DeviceContactExtendedData {
convenience init?(vcard: Data) { convenience init?(vcard: Data) {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { guard let contact = (try? CNContactVCardSerialization.contacts(with: vcard))?.first else {
guard let contact = (try? CNContactVCardSerialization.contacts(with: vcard))?.first else {
return nil
}
self.init(contact: contact)
} else {
return nil return nil
} }
self.init(contact: contact)
} }
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
func asMutableCNContact() -> CNMutableContact { func asMutableCNContact() -> CNMutableContact {
let contact = CNMutableContact() let contact = CNMutableContact()
contact.givenName = self.basicData.firstName contact.givenName = self.basicData.firstName
@ -385,7 +510,6 @@ public extension DeviceContactExtendedData {
return nil return nil
} }
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
convenience init(contact: CNContact) { convenience init(contact: CNContact) {
var phoneNumbers: [DeviceContactPhoneNumberData] = [] var phoneNumbers: [DeviceContactPhoneNumberData] = []
for number in contact.phoneNumbers { for number in contact.phoneNumbers {

View File

@ -5,6 +5,8 @@ import SwiftSignalKit
public typealias DeviceContactStableId = String public typealias DeviceContactStableId = String
public var sharedDisableDeviceContactDataDiffing: Bool = false
public protocol DeviceContactDataManager: AnyObject { public protocol DeviceContactDataManager: AnyObject {
func personNameDisplayOrder() -> Signal<PresentationPersonNameOrder, NoError> func personNameDisplayOrder() -> Signal<PresentationPersonNameOrder, NoError>
func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError>

View File

@ -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",
],
)

View File

@ -0,0 +1,30 @@
#ifndef ContactsHelper_h
#define ContactsHelper_h
#import <Foundation/Foundation.h>
#import <Contacts/Contacts.h>
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<CNChangeHistoryEventVisitor> visitor);
ContactsEnumerateResult * _Nullable ContactsEnumerateRequest(CNContactStore *store, CNContactFetchRequest *fetchRequest, void (^onContact)(CNContact *));
NS_ASSUME_NONNULL_END
#endif /* ContactsHelper_h */

View File

@ -0,0 +1,46 @@
#import <ContactsHelper/ContactsHelper.h>
@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<CNChangeHistoryEventVisitor> visitor) {
NSError *error = nil;
CNFetchResult<NSEnumerator<CNChangeHistoryEvent *> *> *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<NSEnumerator<CNContact *> *> *fetchResult = [store enumeratorForContactFetchRequest:fetchRequest error:&error];
for (CNContact *contact in fetchResult.value) {
onContact(contact);
}
return [[ContactsEnumerateResult alloc] initWithStateToken:fetchResult.currentHistoryToken];
}

View File

@ -7,7 +7,6 @@ import SwiftSignalKit
import Photos import Photos
import CoreLocation import CoreLocation
import Contacts import Contacts
import AddressBook
import UserNotifications import UserNotifications
import CoreTelephony import CoreTelephony
import TelegramPresentationData import TelegramPresentationData
@ -155,29 +154,17 @@ public final class DeviceAccess {
} }
case .contacts: case .contacts:
let status = Signal<AccessType, NoError> { subscriber in let status = Signal<AccessType, NoError> { subscriber in
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { switch CNContactStore.authorizationStatus(for: .contacts) {
switch CNContactStore.authorizationStatus(for: .contacts) { case .notDetermined:
case .notDetermined: subscriber.putNext(.notDetermined)
subscriber.putNext(.notDetermined) case .authorized:
case .authorized: subscriber.putNext(.allowed)
subscriber.putNext(.allowed) case .limited:
case .limited: subscriber.putNext(.limited)
subscriber.putNext(.limited) default:
default: subscriber.putNext(.denied)
subscriber.putNext(.denied)
}
subscriber.putCompletion()
} else {
switch ABAddressBookGetAuthorizationStatus() {
case .notDetermined:
subscriber.putNext(.notDetermined)
case .authorized:
subscriber.putNext(.allowed)
default:
subscriber.putNext(.denied)
}
subscriber.putCompletion()
} }
subscriber.putCompletion()
return EmptyDisposable return EmptyDisposable
} }
return status return status
@ -531,47 +518,22 @@ public final class DeviceAccess {
if let value = value { if let value = value {
completion(value) completion(value)
} else { } else {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { switch CNContactStore.authorizationStatus(for: .contacts) {
switch CNContactStore.authorizationStatus(for: .contacts) { case .notDetermined:
case .notDetermined: let store = CNContactStore()
let store = CNContactStore() store.requestAccess(for: .contacts, completionHandler: { authorized, _ in
store.requestAccess(for: .contacts, completionHandler: { authorized, _ in self.contactsPromise.set(.single(authorized))
self.contactsPromise.set(.single(authorized)) completion(authorized)
completion(authorized) })
}) case .authorized:
case .authorized: self.contactsPromise.set(.single(true))
self.contactsPromise.set(.single(true)) completion(true)
completion(true) case .limited:
case .limited: self.contactsPromise.set(.single(true))
self.contactsPromise.set(.single(true)) completion(true)
completion(true) default:
default: self.contactsPromise.set(.single(false))
self.contactsPromise.set(.single(false)) completion(false)
completion(false)
}
} else {
switch ABAddressBookGetAuthorizationStatus() {
case .notDetermined:
var error: Unmanaged<CFError>?
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)
}
} }
} }
}) })

View File

@ -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;

View File

@ -584,6 +584,7 @@ private enum SharedDataKeyValues: Int32 {
case countriesList = 7 case countriesList = 7
case wallapersState = 8 case wallapersState = 8
case chatThemes = 10 case chatThemes = 10
case deviceContacts = 11
} }
public struct SharedDataKeys { public struct SharedDataKeys {
@ -640,6 +641,12 @@ public struct SharedDataKeys {
key.setInt32(0, value: SharedDataKeyValues.chatThemes.rawValue) key.setInt32(0, value: SharedDataKeyValues.chatThemes.rawValue)
return key 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 { public func applicationSpecificItemCacheCollectionId(_ value: Int8) -> Int8 {

View File

@ -4,7 +4,6 @@ import SwiftSignalKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import Contacts import Contacts
import AddressBook
import Display import Display
import TelegramUIPreferences import TelegramUIPreferences
import AppBundle import AppBundle
@ -201,19 +200,11 @@ private func currentDateTimeFormat() -> PresentationDateTimeFormat {
} }
private func currentPersonNameSortOrder() -> PresentationPersonNameOrder { private func currentPersonNameSortOrder() -> PresentationPersonNameOrder {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { switch CNContactsUserDefaults.shared().sortOrder {
switch CNContactsUserDefaults.shared().sortOrder { case .givenName:
case .givenName:
return .firstLast
default:
return .lastFirst
}
} else {
if ABPersonGetSortOrdering() == kABPersonSortByFirstName {
return .firstLast return .firstLast
} else { default:
return .lastFirst return .lastFirst
}
} }
} }

View File

@ -482,6 +482,7 @@ swift_library(
"//submodules/TelegramUI/Components/ComposeTodoScreen", "//submodules/TelegramUI/Components/ComposeTodoScreen",
"//submodules/TelegramUI/Components/SuggestedPostApproveAlert", "//submodules/TelegramUI/Components/SuggestedPostApproveAlert",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/ContactsHelper",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View File

@ -359,6 +359,10 @@ public final class AccountContextImpl: AccountContext {
self.appConfigurationDisposable = (self._appConfiguration.get() self.appConfigurationDisposable = (self._appConfiguration.get()
|> deliverOnMainQueue).start(next: { value in |> deliverOnMainQueue).start(next: { value in
let _ = currentAppConfiguration.swap(value) 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 let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode

View File

@ -7,6 +7,8 @@ import TelegramUIPreferences
import DeviceAccess import DeviceAccess
import AccountContext import AccountContext
import PhoneNumberFormat import PhoneNumberFormat
import ContactsHelper
import AccountContext
private protocol DeviceContactDataContext { private protocol DeviceContactDataContext {
func personNameDisplayOrder() -> PresentationPersonNameOrder func personNameDisplayOrder() -> PresentationPersonNameOrder
@ -17,62 +19,222 @@ private protocol DeviceContactDataContext {
func deleteContactWithAppSpecificReference(peerId: PeerId) func deleteContactWithAppSpecificReference(peerId: PeerId)
} }
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
private final class DeviceContactDataModernContext: DeviceContactDataContext { private final class DeviceContactDataModernContext: DeviceContactDataContext {
let queue: Queue
let accountManager: AccountManager<TelegramAccountManagerTypes>
let store = CNContactStore() let store = CNContactStore()
var updateHandle: NSObjectProtocol? var updateHandle: NSObjectProtocol?
var currentContacts: [DeviceContactStableId: DeviceContactBasicData] = [:] var currentContacts: [DeviceContactStableId: DeviceContactBasicData] = [:]
var currentAppSpecificReferences: [PeerId: DeviceContactBasicDataWithReference] = [:] var currentAppSpecificReferences: [PeerId: DeviceContactBasicDataWithReference] = [:]
init(queue: Queue, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void, appSpecificReferencesUpdated: @escaping ([PeerId: DeviceContactBasicDataWithReference]) -> Void) { private var retrieveContactsDisposable: Disposable?
let (contacts, references) = self.retrieveContacts()
self.currentContacts = contacts init(queue: Queue, accountManager: AccountManager<TelegramAccountManagerTypes>, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void, appSpecificReferencesUpdated: @escaping ([PeerId: DeviceContactBasicDataWithReference]) -> Void) {
self.currentAppSpecificReferences = references self.queue = queue
updated(self.currentContacts) self.accountManager = accountManager
appSpecificReferencesUpdated(self.currentAppSpecificReferences)
let handle = NotificationCenter.default.addObserver(forName: NSNotification.Name.CNContactStoreDidChange, object: nil, queue: nil, using: { [weak self] _ in self.retrieveContactsDisposable?.dispose()
queue.async { self.retrieveContactsDisposable = (self.retrieveContacts()
guard let strongSelf = self else { |> deliverOn(self.queue)).startStrict(next: { [weak self] contacts, references in
return guard let 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)
}
} }
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 { deinit {
if let updateHandle = updateHandle { self.retrieveContactsDisposable?.dispose()
if let updateHandle = self.updateHandle {
NotificationCenter.default.removeObserver(updateHandle) NotificationCenter.default.removeObserver(updateHandle)
} }
} }
private func retrieveContacts() -> ([DeviceContactStableId: DeviceContactBasicData], [PeerId: DeviceContactBasicDataWithReference]) { private func retrieveContacts() -> Signal<([DeviceContactStableId: DeviceContactBasicData], [PeerId: DeviceContactBasicDataWithReference]), NoError> {
let keysToFetch: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor, CNContactUrlAddressesKey as CNKeyDescriptor] 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
let request = CNContactFetchRequest(keysToFetch: keysToFetch) init(onDropEverything: @escaping () -> Void, onAdd: @escaping (CNContact) -> Void, onUpdate: @escaping (CNContact) -> Void, onDelete: @escaping (String) -> Void) {
request.unifyResults = true self.onDropEverything = onDropEverything
self.onAdd = onAdd
self.onUpdate = onUpdate
self.onDelete = onDelete
var result: [DeviceContactStableId: DeviceContactBasicData] = [:] super.init()
var references: [PeerId: DeviceContactBasicDataWithReference] = [:] }
let _ = try? self.store.enumerateContacts(with: request, usingBlock: { contact, _ in
let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact) func visit(_ event: CNChangeHistoryDropEverythingEvent) {
result[stableIdAndContact.0] = stableIdAndContact.1 self.onDropEverything()
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) 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<PeerId>] = [:]
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) { private static func parseContact(_ contact: CNContact) -> (DeviceContactStableId, DeviceContactBasicData) {
@ -323,6 +485,7 @@ private final class BasicDataForNormalizedNumberContext {
private final class DeviceContactDataManagerPrivateImpl { private final class DeviceContactDataManagerPrivateImpl {
private let queue: Queue private let queue: Queue
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private var accessInitialized = false private var accessInitialized = false
@ -345,8 +508,9 @@ private final class DeviceContactDataManagerPrivateImpl {
private let importableContactsSubscribers = Bag<([DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]) -> Void>() private let importableContactsSubscribers = Bag<([DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]) -> Void>()
private let appSpecificReferencesSubscribers = Bag<([PeerId: DeviceContactBasicDataWithReference]) -> Void>() private let appSpecificReferencesSubscribers = Bag<([PeerId: DeviceContactBasicDataWithReference]) -> Void>()
init(queue: Queue) { init(queue: Queue, accountManager: AccountManager<TelegramAccountManagerTypes>) {
self.queue = queue self.queue = queue
self.accountManager = accountManager
self.accessDisposable = (DeviceAccess.authorizationStatus(subject: .contacts) self.accessDisposable = (DeviceAccess.authorizationStatus(subject: .contacts)
|> delay(2.0, queue: .mainQueue()) |> delay(2.0, queue: .mainQueue())
|> deliverOn(self.queue)).startStrict(next: { [weak self] authorizationStatus in |> deliverOn(self.queue)).startStrict(next: { [weak self] authorizationStatus in
@ -355,7 +519,7 @@ private final class DeviceContactDataManagerPrivateImpl {
} }
strongSelf.accessInitialized = true strongSelf.accessInitialized = true
if authorizationStatus == .allowed { 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 { guard let strongSelf = self else {
return return
} }
@ -605,10 +769,10 @@ public final class DeviceContactDataManagerImpl: DeviceContactDataManager {
private let queue = Queue() private let queue = Queue()
private let impl: QueueLocalObject<DeviceContactDataManagerPrivateImpl> private let impl: QueueLocalObject<DeviceContactDataManagerPrivateImpl>
init() { init(accountManager: AccountManager<TelegramAccountManagerTypes>) {
let queue = self.queue let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: { self.impl = QueueLocalObject(queue: queue, generate: {
return DeviceContactDataManagerPrivateImpl(queue: queue) return DeviceContactDataManagerPrivateImpl(queue: queue, accountManager: accountManager)
}) })
} }

View File

@ -316,7 +316,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
if applicationBindings.isMainApp { if applicationBindings.isMainApp {
self.locationManager = DeviceLocationManager(queue: Queue.mainQueue()) self.locationManager = DeviceLocationManager(queue: Queue.mainQueue())
self.contactDataManager = DeviceContactDataManagerImpl() self.contactDataManager = DeviceContactDataManagerImpl(accountManager: accountManager)
} else { } else {
self.locationManager = nil self.locationManager = nil
self.contactDataManager = nil self.contactDataManager = nil