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/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramCore/FlatBuffers",
"//submodules/TelegramCore/FlatSerialization",
],
visibility = [
"//visibility:public",

View File

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

View File

@ -5,6 +5,8 @@ import SwiftSignalKit
public typealias DeviceContactStableId = String
public var sharedDisableDeviceContactDataDiffing: Bool = false
public protocol DeviceContactDataManager: AnyObject {
func personNameDisplayOrder() -> Signal<PresentationPersonNameOrder, 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 CoreLocation
import Contacts
import AddressBook
import UserNotifications
import CoreTelephony
import TelegramPresentationData
@ -155,29 +154,17 @@ public final class DeviceAccess {
}
case .contacts:
let status = Signal<AccessType, NoError> { 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<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)
}
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)
}
}
})

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 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 {

View File

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

View File

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

View File

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

View File

@ -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<TelegramAccountManagerTypes>
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<TelegramAccountManagerTypes>, 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<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) {
@ -323,6 +485,7 @@ private final class BasicDataForNormalizedNumberContext {
private final class DeviceContactDataManagerPrivateImpl {
private let queue: Queue
private let accountManager: AccountManager<TelegramAccountManagerTypes>
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<TelegramAccountManagerTypes>) {
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<DeviceContactDataManagerPrivateImpl>
init() {
init(accountManager: AccountManager<TelegramAccountManagerTypes>) {
let queue = self.queue
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 {
self.locationManager = DeviceLocationManager(queue: Queue.mainQueue())
self.contactDataManager = DeviceContactDataManagerImpl()
self.contactDataManager = DeviceContactDataManagerImpl(accountManager: accountManager)
} else {
self.locationManager = nil
self.contactDataManager = nil