mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
910 lines
39 KiB
Swift
910 lines
39 KiB
Swift
import Foundation
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SyncCore
|
|
import Contacts
|
|
import AddressBook
|
|
import TelegramUIPreferences
|
|
import DeviceAccess
|
|
import AccountContext
|
|
import PhoneNumberFormat
|
|
|
|
private protocol DeviceContactDataContext {
|
|
func personNameDisplayOrder() -> PresentationPersonNameOrder
|
|
func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData?
|
|
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData?
|
|
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData?
|
|
func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)?
|
|
func deleteContactWithAppSpecificReference(peerId: PeerId)
|
|
}
|
|
|
|
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
|
|
private final class DeviceContactDataModernContext: DeviceContactDataContext {
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
self.updateHandle = handle
|
|
}
|
|
|
|
deinit {
|
|
if let updateHandle = 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)
|
|
}
|
|
}
|
|
})
|
|
return (result, references)
|
|
}
|
|
|
|
private static func parseContact(_ contact: CNContact) -> (DeviceContactStableId, DeviceContactBasicData) {
|
|
var phoneNumbers: [DeviceContactPhoneNumberData] = []
|
|
for number in contact.phoneNumbers {
|
|
phoneNumbers.append(DeviceContactPhoneNumberData(label: number.label ?? "", value: number.value.stringValue))
|
|
}
|
|
return (contact.identifier, DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers))
|
|
}
|
|
|
|
func personNameDisplayOrder() -> PresentationPersonNameOrder {
|
|
switch CNContactFormatter.nameOrder(for: CNContact()) {
|
|
case .givenNameFirst:
|
|
return .firstLast
|
|
default:
|
|
return .lastFirst
|
|
}
|
|
}
|
|
|
|
func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
|
|
let keysToFetch: [CNKeyDescriptor] = [
|
|
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
|
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
CNContactEmailAddressesKey as CNKeyDescriptor,
|
|
CNContactBirthdayKey as CNKeyDescriptor,
|
|
CNContactSocialProfilesKey as CNKeyDescriptor,
|
|
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
|
|
CNContactPostalAddressesKey as CNKeyDescriptor,
|
|
CNContactUrlAddressesKey as CNKeyDescriptor,
|
|
CNContactOrganizationNameKey as CNKeyDescriptor,
|
|
CNContactJobTitleKey as CNKeyDescriptor,
|
|
CNContactDepartmentNameKey as CNKeyDescriptor
|
|
]
|
|
|
|
guard let contact = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
|
|
return nil
|
|
}
|
|
|
|
return DeviceContactExtendedData(contact: contact)
|
|
}
|
|
|
|
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
|
|
let keysToFetch: [CNKeyDescriptor] = [
|
|
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
|
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
CNContactEmailAddressesKey as CNKeyDescriptor,
|
|
CNContactBirthdayKey as CNKeyDescriptor,
|
|
CNContactSocialProfilesKey as CNKeyDescriptor,
|
|
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
|
|
CNContactPostalAddressesKey as CNKeyDescriptor,
|
|
CNContactUrlAddressesKey as CNKeyDescriptor,
|
|
CNContactOrganizationNameKey as CNKeyDescriptor,
|
|
CNContactJobTitleKey as CNKeyDescriptor,
|
|
CNContactDepartmentNameKey as CNKeyDescriptor
|
|
]
|
|
|
|
guard let current = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
|
|
return nil
|
|
}
|
|
|
|
let contact = contactData.asMutableCNContact()
|
|
|
|
let mutableContact = current.mutableCopy() as! CNMutableContact
|
|
mutableContact.givenName = contact.givenName
|
|
mutableContact.familyName = contact.familyName
|
|
|
|
var phoneNumbers = mutableContact.phoneNumbers
|
|
for phoneNumber in contact.phoneNumbers.reversed() {
|
|
var found = false
|
|
inner: for n in phoneNumbers {
|
|
if n.value.stringValue == phoneNumber.value.stringValue {
|
|
found = true
|
|
break inner
|
|
}
|
|
}
|
|
if !found {
|
|
phoneNumbers.insert(phoneNumber, at: 0)
|
|
}
|
|
}
|
|
mutableContact.phoneNumbers = phoneNumbers
|
|
|
|
var urlAddresses = mutableContact.urlAddresses
|
|
for urlAddress in contact.urlAddresses.reversed() {
|
|
var found = false
|
|
inner: for n in urlAddresses {
|
|
if n.value.isEqual(urlAddress.value) {
|
|
found = true
|
|
break inner
|
|
}
|
|
}
|
|
if !found {
|
|
urlAddresses.insert(urlAddress, at: 0)
|
|
}
|
|
}
|
|
mutableContact.urlAddresses = urlAddresses
|
|
|
|
var emailAddresses = mutableContact.emailAddresses
|
|
for emailAddress in contact.emailAddresses.reversed() {
|
|
var found = false
|
|
inner: for n in emailAddresses {
|
|
if n.value.isEqual(emailAddress.value) {
|
|
found = true
|
|
break inner
|
|
}
|
|
}
|
|
if !found {
|
|
emailAddresses.insert(emailAddress, at: 0)
|
|
}
|
|
}
|
|
mutableContact.emailAddresses = emailAddresses
|
|
|
|
var postalAddresses = mutableContact.postalAddresses
|
|
for postalAddress in contact.postalAddresses.reversed() {
|
|
var found = false
|
|
inner: for n in postalAddresses {
|
|
if n.value.isEqual(postalAddress.value) {
|
|
found = true
|
|
break inner
|
|
}
|
|
}
|
|
if !found {
|
|
postalAddresses.insert(postalAddress, at: 0)
|
|
}
|
|
}
|
|
mutableContact.postalAddresses = postalAddresses
|
|
|
|
if contact.birthday != nil {
|
|
mutableContact.birthday = contact.birthday
|
|
}
|
|
|
|
var socialProfiles = mutableContact.socialProfiles
|
|
for socialProfile in contact.socialProfiles.reversed() {
|
|
var found = false
|
|
inner: for n in socialProfiles {
|
|
if n.value.username.lowercased() == socialProfile.value.username.lowercased() && n.value.service.lowercased() == socialProfile.value.service.lowercased() {
|
|
found = true
|
|
break inner
|
|
}
|
|
}
|
|
if !found {
|
|
socialProfiles.insert(socialProfile, at: 0)
|
|
}
|
|
}
|
|
mutableContact.socialProfiles = socialProfiles
|
|
|
|
var instantMessageAddresses = mutableContact.instantMessageAddresses
|
|
for instantMessageAddress in contact.instantMessageAddresses.reversed() {
|
|
var found = false
|
|
inner: for n in instantMessageAddresses {
|
|
if n.value.isEqual(instantMessageAddress.value) {
|
|
found = true
|
|
break inner
|
|
}
|
|
}
|
|
if !found {
|
|
instantMessageAddresses.insert(instantMessageAddress, at: 0)
|
|
}
|
|
}
|
|
mutableContact.instantMessageAddresses = instantMessageAddresses
|
|
|
|
let saveRequest = CNSaveRequest()
|
|
saveRequest.update(mutableContact)
|
|
let _ = try? self.store.execute(saveRequest)
|
|
|
|
return DeviceContactExtendedData(contact: mutableContact)
|
|
}
|
|
|
|
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
|
|
let keysToFetch: [CNKeyDescriptor] = [
|
|
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
|
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
CNContactEmailAddressesKey as CNKeyDescriptor,
|
|
CNContactBirthdayKey as CNKeyDescriptor,
|
|
CNContactSocialProfilesKey as CNKeyDescriptor,
|
|
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
|
|
CNContactPostalAddressesKey as CNKeyDescriptor,
|
|
CNContactUrlAddressesKey as CNKeyDescriptor,
|
|
CNContactOrganizationNameKey as CNKeyDescriptor,
|
|
CNContactJobTitleKey as CNKeyDescriptor,
|
|
CNContactDepartmentNameKey as CNKeyDescriptor
|
|
]
|
|
|
|
guard let current = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
|
|
return nil
|
|
}
|
|
|
|
let mutableContact = current.mutableCopy() as! CNMutableContact
|
|
|
|
var phoneNumbers = mutableContact.phoneNumbers
|
|
let appendPhoneNumbers: [CNLabeledValue<CNPhoneNumber>] = [CNLabeledValue<CNPhoneNumber>(label: phoneNumber.label, value: CNPhoneNumber(stringValue: phoneNumber.value))]
|
|
for appendPhoneNumber in appendPhoneNumbers {
|
|
var found = false
|
|
inner: for n in phoneNumbers {
|
|
if n.value.stringValue == appendPhoneNumber.value.stringValue {
|
|
found = true
|
|
break inner
|
|
}
|
|
}
|
|
if !found {
|
|
phoneNumbers.insert(appendPhoneNumber, at: 0)
|
|
}
|
|
}
|
|
mutableContact.phoneNumbers = phoneNumbers
|
|
|
|
let saveRequest = CNSaveRequest()
|
|
saveRequest.update(mutableContact)
|
|
let _ = try? self.store.execute(saveRequest)
|
|
|
|
return DeviceContactExtendedData(contact: mutableContact)
|
|
}
|
|
|
|
func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)? {
|
|
let saveRequest = CNSaveRequest()
|
|
let mutableContact = contactData.asMutableCNContact()
|
|
saveRequest.add(mutableContact, toContainerWithIdentifier: nil)
|
|
let _ = try? self.store.execute(saveRequest)
|
|
|
|
return (mutableContact.identifier, contactData)
|
|
}
|
|
|
|
func deleteContactWithAppSpecificReference(peerId: PeerId) {
|
|
guard let reference = self.currentAppSpecificReferences[peerId] else {
|
|
return
|
|
}
|
|
guard let current = try? self.store.unifiedContact(withIdentifier: reference.stableId, keysToFetch: []) else {
|
|
return
|
|
}
|
|
|
|
let saveRequest = CNSaveRequest()
|
|
saveRequest.delete(current.mutableCopy() as! CNMutableContact)
|
|
let _ = try? self.store.execute(saveRequest)
|
|
}
|
|
}
|
|
|
|
private func withAddressBook(_ f: (ABAddressBook) -> Void) {
|
|
let addressBookRef = ABAddressBookCreateWithOptions(nil, nil)
|
|
|
|
if let addressBook = addressBookRef?.takeRetainedValue() {
|
|
f(addressBook)
|
|
}
|
|
}
|
|
|
|
private final class DeviceContactDataLegacyContext: DeviceContactDataContext {
|
|
var currentContacts: [DeviceContactStableId: DeviceContactBasicData] = [:]
|
|
|
|
init(queue: Queue, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) {
|
|
self.currentContacts = self.retrieveContacts()
|
|
updated(self.currentContacts)
|
|
/*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 = strongSelf.retrieveContacts()
|
|
if strongSelf.currentContacts != contacts {
|
|
strongSelf.currentContacts = contacts
|
|
updated(strongSelf.currentContacts)
|
|
}
|
|
}
|
|
})*/
|
|
//self.updateHandle = handle
|
|
}
|
|
|
|
deinit {
|
|
/*if let updateHandle = updateHandle {
|
|
NotificationCenter.default.removeObserver(updateHandle)
|
|
}*/
|
|
}
|
|
|
|
private func retrieveContacts() -> [DeviceContactStableId: DeviceContactBasicData] {
|
|
var result: [DeviceContactStableId: DeviceContactBasicData] = [:]
|
|
withAddressBook { addressBook in
|
|
guard let peopleRef = ABAddressBookCopyArrayOfAllPeople(addressBook)?.takeRetainedValue() else {
|
|
return
|
|
}
|
|
|
|
for recordRef in peopleRef as NSArray {
|
|
let record = recordRef as ABRecord
|
|
let (stableId, basicData) = DeviceContactDataLegacyContext.parseContact(record)
|
|
result[stableId] = basicData
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
private func getContactById(stableId: String) -> ABRecord? {
|
|
let recordId: ABRecordID
|
|
if stableId.hasPrefix("ab-"), let idValue = Int(String(stableId[stableId.index(stableId.startIndex, offsetBy: 3)])) {
|
|
recordId = Int32(clamping: idValue)
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
var result: ABRecord?
|
|
withAddressBook { addressBook in
|
|
result = ABAddressBookGetPersonWithRecordID(addressBook, recordId)?.takeUnretainedValue()
|
|
}
|
|
return result
|
|
}
|
|
|
|
private static func parseContact(_ contact: ABRecord) -> (DeviceContactStableId, DeviceContactBasicData) {
|
|
let stableId = "ab-\(ABRecordGetRecordID(contact))"
|
|
var firstName = ""
|
|
var lastName = ""
|
|
if let value = ABRecordCopyValue(contact, kABPersonFirstNameProperty)?.takeRetainedValue() {
|
|
firstName = value as! CFString as String
|
|
}
|
|
if let value = ABRecordCopyValue(contact, kABPersonLastNameProperty)?.takeRetainedValue() {
|
|
lastName = value as! CFString as String
|
|
}
|
|
|
|
var phoneNumbers: [DeviceContactPhoneNumberData] = []
|
|
if let value = ABRecordCopyValue(contact, kABPersonPhoneProperty)?.takeRetainedValue() {
|
|
let phones = value as ABMultiValue
|
|
let count = ABMultiValueGetCount(phones)
|
|
for i in 0 ..< count {
|
|
if let phoneRef = ABMultiValueCopyValueAtIndex(phones, i)?.takeRetainedValue() {
|
|
let phone = phoneRef as! CFString as String
|
|
var label = ""
|
|
if let labelRef = ABMultiValueCopyLabelAtIndex(phones, i)?.takeRetainedValue() {
|
|
label = labelRef as String
|
|
}
|
|
phoneNumbers.append(DeviceContactPhoneNumberData(label: label, value: phone))
|
|
}
|
|
}
|
|
}
|
|
|
|
return (stableId, DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: phoneNumbers))
|
|
}
|
|
|
|
func personNameDisplayOrder() -> PresentationPersonNameOrder {
|
|
if ABPersonGetCompositeNameFormat() == kABPersonCompositeNameFormatFirstNameFirst {
|
|
return .firstLast
|
|
} else {
|
|
return .lastFirst
|
|
}
|
|
}
|
|
|
|
func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
|
|
if let contact = self.getContactById(stableId: stableId) {
|
|
let basicData = DeviceContactDataLegacyContext.parseContact(contact).1
|
|
return DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
|
|
return nil
|
|
}
|
|
|
|
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
|
|
return nil
|
|
}
|
|
|
|
func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)? {
|
|
var result: (DeviceContactStableId, DeviceContactExtendedData)?
|
|
withAddressBook { addressBook in
|
|
let contact = ABPersonCreate()?.takeRetainedValue()
|
|
ABRecordSetValue(contact, kABPersonFirstNameProperty, contactData.basicData.firstName as CFString, nil)
|
|
ABRecordSetValue(contact, kABPersonLastNameProperty, contactData.basicData.lastName as CFString, nil)
|
|
|
|
let phones = ABMultiValueCreateMutable(ABPropertyType(kABMultiStringPropertyType))?.takeRetainedValue()
|
|
for phone in contactData.basicData.phoneNumbers {
|
|
ABMultiValueAddValueAndLabel(phones, phone.value as CFString, phone.label as CFString, nil)
|
|
}
|
|
ABRecordSetValue(contact, kABPersonPhoneProperty, phones, nil)
|
|
|
|
if ABAddressBookAddRecord(addressBook, contact, nil) {
|
|
ABAddressBookSave(addressBook, nil)
|
|
|
|
let stableId = "ab-\(ABRecordGetRecordID(contact))"
|
|
if let contact = self.getContactById(stableId: stableId) {
|
|
let parsedContact = DeviceContactDataLegacyContext.parseContact(contact).1
|
|
result = (stableId, DeviceContactExtendedData(basicData: parsedContact, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: ""))
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func deleteContactWithAppSpecificReference(peerId: PeerId) {
|
|
}
|
|
}
|
|
|
|
private final class ExtendedContactDataContext {
|
|
var value: DeviceContactExtendedData?
|
|
let subscribers = Bag<(DeviceContactExtendedData) -> Void>()
|
|
}
|
|
|
|
private final class BasicDataForNormalizedNumberContext {
|
|
var value: [(DeviceContactStableId, DeviceContactBasicData)]
|
|
let subscribers = Bag<([(DeviceContactStableId, DeviceContactBasicData)]) -> Void>()
|
|
|
|
init(value: [(DeviceContactStableId, DeviceContactBasicData)]) {
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
private final class DeviceContactDataManagerPrivateImpl {
|
|
private let queue: Queue
|
|
|
|
private var accessInitialized = false
|
|
|
|
private var dataContext: DeviceContactDataContext?
|
|
let personNameDisplayOrder = ValuePromise<PresentationPersonNameOrder>()
|
|
private var extendedContexts: [DeviceContactStableId: ExtendedContactDataContext] = [:]
|
|
|
|
private var stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData] = [:]
|
|
private var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:]
|
|
private var appSpecificReferences: [PeerId: DeviceContactBasicDataWithReference] = [:]
|
|
private var stableIdToAppSpecificReference: [DeviceContactStableId: PeerId] = [:]
|
|
|
|
private var importableContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData] = [:]
|
|
|
|
private var accessDisposable: Disposable?
|
|
private let dataDisposable = MetaDisposable()
|
|
|
|
private let basicDataSubscribers = Bag<([DeviceContactStableId: DeviceContactBasicData]) -> Void>()
|
|
private var basicDataForNormalizedNumberContexts: [DeviceContactNormalizedPhoneNumber: BasicDataForNormalizedNumberContext] = [:]
|
|
private let importableContactsSubscribers = Bag<([DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]) -> Void>()
|
|
private let appSpecificReferencesSubscribers = Bag<([PeerId: DeviceContactBasicDataWithReference]) -> Void>()
|
|
|
|
init(queue: Queue) {
|
|
self.queue = queue
|
|
self.accessDisposable = (DeviceAccess.authorizationStatus(subject: .contacts)
|
|
|> delay(2.0, queue: .mainQueue())
|
|
|> deliverOn(self.queue)).start(next: { [weak self] authorizationStatus in
|
|
guard let strongSelf = self, authorizationStatus != .notDetermined else {
|
|
return
|
|
}
|
|
strongSelf.accessInitialized = true
|
|
if authorizationStatus == .allowed {
|
|
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
|
let dataContext = DeviceContactDataModernContext(queue: strongSelf.queue, updated: { stableIdToBasicContactData in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateAll(stableIdToBasicContactData)
|
|
}, appSpecificReferencesUpdated: { appSpecificReferences in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateAppSpecificReferences(appSpecificReferences: appSpecificReferences)
|
|
})
|
|
strongSelf.dataContext = dataContext
|
|
strongSelf.personNameDisplayOrder.set(dataContext.personNameDisplayOrder())
|
|
} else {
|
|
let dataContext = DeviceContactDataLegacyContext(queue: strongSelf.queue, updated: { stableIdToBasicContactData in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateAll(stableIdToBasicContactData)
|
|
})
|
|
strongSelf.dataContext = dataContext
|
|
strongSelf.personNameDisplayOrder.set(dataContext.personNameDisplayOrder())
|
|
}
|
|
} else {
|
|
strongSelf.updateAll([:])
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.accessDisposable?.dispose()
|
|
}
|
|
|
|
private func updateAll(_ stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData]) {
|
|
self.stableIdToBasicContactData = stableIdToBasicContactData
|
|
var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:]
|
|
for (stableId, basicData) in self.stableIdToBasicContactData {
|
|
for phoneNumber in basicData.phoneNumbers {
|
|
let normalizedPhoneNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value))
|
|
if normalizedPhoneNumberToStableId[normalizedPhoneNumber] == nil {
|
|
normalizedPhoneNumberToStableId[normalizedPhoneNumber] = []
|
|
}
|
|
normalizedPhoneNumberToStableId[normalizedPhoneNumber]!.append(stableId)
|
|
}
|
|
}
|
|
self.normalizedPhoneNumberToStableId = normalizedPhoneNumberToStableId
|
|
for f in self.basicDataSubscribers.copyItems() {
|
|
f(self.stableIdToBasicContactData)
|
|
}
|
|
|
|
for (normalizedNumber, context) in self.basicDataForNormalizedNumberContexts {
|
|
var value: [(DeviceContactStableId, DeviceContactBasicData)] = []
|
|
if let ids = self.normalizedPhoneNumberToStableId[normalizedNumber] {
|
|
for id in ids {
|
|
if let basicData = self.stableIdToBasicContactData[id] {
|
|
value.append((id, basicData))
|
|
}
|
|
}
|
|
}
|
|
|
|
var updated = false
|
|
if value.count != context.value.count {
|
|
updated = true
|
|
} else {
|
|
for i in 0 ..< value.count {
|
|
if value[i].0 != context.value[i].0 || value[i].1 != context.value[i].1 {
|
|
updated = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
context.value = value
|
|
for f in context.subscribers.copyItems() {
|
|
f(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
var importableContactData: [String: (DeviceContactStableId, ImportableDeviceContactData)] = [:]
|
|
for (stableId, basicData) in self.stableIdToBasicContactData {
|
|
for phoneNumber in basicData.phoneNumbers {
|
|
var replace = false
|
|
if let current = importableContactData[phoneNumber.value] {
|
|
if stableId < current.0 {
|
|
replace = true
|
|
}
|
|
} else {
|
|
replace = true
|
|
}
|
|
if replace {
|
|
importableContactData[phoneNumber.value] = (stableId, ImportableDeviceContactData(firstName: basicData.firstName, lastName: basicData.lastName))
|
|
}
|
|
}
|
|
}
|
|
var importabledContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData] = [:]
|
|
for (number, data) in importableContactData {
|
|
importabledContacts[DeviceContactNormalizedPhoneNumber(rawValue: number)] = data.1
|
|
}
|
|
self.importableContacts = importabledContacts
|
|
for f in self.importableContactsSubscribers.copyItems() {
|
|
f(importableContacts)
|
|
}
|
|
}
|
|
|
|
private func updateAppSpecificReferences(appSpecificReferences: [PeerId: DeviceContactBasicDataWithReference]) {
|
|
self.appSpecificReferences = appSpecificReferences
|
|
var stableIdToAppSpecificReference: [DeviceContactStableId: PeerId] = [:]
|
|
for (peerId, value) in appSpecificReferences {
|
|
stableIdToAppSpecificReference[value.stableId] = peerId
|
|
}
|
|
self.stableIdToAppSpecificReference = stableIdToAppSpecificReference
|
|
for f in self.appSpecificReferencesSubscribers.copyItems() {
|
|
f(appSpecificReferences)
|
|
}
|
|
}
|
|
|
|
func basicData(updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) -> Disposable {
|
|
let queue = self.queue
|
|
|
|
let index = self.basicDataSubscribers.add({ data in
|
|
updated(data)
|
|
})
|
|
|
|
updated(self.stableIdToBasicContactData)
|
|
|
|
return ActionDisposable { [weak self] in
|
|
queue.async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.basicDataSubscribers.remove(index)
|
|
}
|
|
}
|
|
}
|
|
|
|
func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber, updated: @escaping ([(DeviceContactStableId, DeviceContactBasicData)]) -> Void) -> Disposable {
|
|
let queue = self.queue
|
|
let context: BasicDataForNormalizedNumberContext
|
|
if let current = self.basicDataForNormalizedNumberContexts[normalizedNumber] {
|
|
context = current
|
|
} else {
|
|
var value: [(DeviceContactStableId, DeviceContactBasicData)] = []
|
|
if let ids = self.normalizedPhoneNumberToStableId[normalizedNumber] {
|
|
for id in ids {
|
|
if let basicData = self.stableIdToBasicContactData[id] {
|
|
value.append((id, basicData))
|
|
}
|
|
}
|
|
}
|
|
context = BasicDataForNormalizedNumberContext(value: value)
|
|
self.basicDataForNormalizedNumberContexts[normalizedNumber] = context
|
|
}
|
|
updated(context.value)
|
|
let index = context.subscribers.add({ value in
|
|
updated(value)
|
|
})
|
|
return ActionDisposable { [weak self, weak context] in
|
|
queue.async {
|
|
if let strongSelf = self, let foundContext = strongSelf.basicDataForNormalizedNumberContexts[normalizedNumber], foundContext === context {
|
|
foundContext.subscribers.remove(index)
|
|
if foundContext.subscribers.isEmpty {
|
|
strongSelf.basicDataForNormalizedNumberContexts.removeValue(forKey: normalizedNumber)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func extendedData(stableId: String, updated: @escaping (DeviceContactExtendedData?) -> Void) -> Disposable {
|
|
let current = self.dataContext?.getExtendedContactData(stableId: stableId)
|
|
updated(current)
|
|
|
|
return ActionDisposable {
|
|
}
|
|
}
|
|
|
|
func importable(updated: @escaping ([DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]) -> Void) -> Disposable {
|
|
let queue = self.queue
|
|
|
|
let index = self.importableContactsSubscribers.add({ data in
|
|
updated(data)
|
|
})
|
|
if self.accessInitialized {
|
|
updated(self.importableContacts)
|
|
}
|
|
|
|
return ActionDisposable { [weak self] in
|
|
queue.async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.importableContactsSubscribers.remove(index)
|
|
}
|
|
}
|
|
}
|
|
|
|
func appSpecificReferences(updated: @escaping ([PeerId: DeviceContactBasicDataWithReference]) -> Void) -> Disposable {
|
|
let queue = self.queue
|
|
|
|
let index = self.appSpecificReferencesSubscribers.add({ data in
|
|
updated(data)
|
|
})
|
|
if self.accessInitialized {
|
|
updated(self.appSpecificReferences)
|
|
}
|
|
|
|
return ActionDisposable { [weak self] in
|
|
queue.async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.appSpecificReferencesSubscribers.remove(index)
|
|
}
|
|
}
|
|
}
|
|
|
|
func search(query: String, updated: @escaping ([DeviceContactStableId: (DeviceContactBasicData, PeerId?)]) -> Void) -> Disposable {
|
|
let normalizedQuery = query.lowercased()
|
|
var result: [DeviceContactStableId: (DeviceContactBasicData, PeerId?)] = [:]
|
|
for (stableId, basicData) in self.stableIdToBasicContactData {
|
|
if basicData.firstName.lowercased().hasPrefix(normalizedQuery) || basicData.lastName.lowercased().hasPrefix(normalizedQuery) {
|
|
result[stableId] = (basicData, self.stableIdToAppSpecificReference[stableId])
|
|
}
|
|
}
|
|
updated(result)
|
|
return EmptyDisposable
|
|
}
|
|
|
|
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId, completion: @escaping (DeviceContactExtendedData?) -> Void) {
|
|
let result = self.dataContext?.appendContactData(contactData, to: stableId)
|
|
completion(result)
|
|
}
|
|
|
|
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId, completion: @escaping (DeviceContactExtendedData?) -> Void) {
|
|
let result = self.dataContext?.appendPhoneNumber(phoneNumber, to: stableId)
|
|
completion(result)
|
|
}
|
|
|
|
func createContactWithData(_ contactData: DeviceContactExtendedData, completion: @escaping ((DeviceContactStableId, DeviceContactExtendedData)?) -> Void) {
|
|
let result = self.dataContext?.createContactWithData(contactData)
|
|
completion(result)
|
|
}
|
|
|
|
func deleteContactWithAppSpecificReference(peerId: PeerId, completion: @escaping () -> Void) {
|
|
self.dataContext?.deleteContactWithAppSpecificReference(peerId: peerId)
|
|
completion()
|
|
}
|
|
}
|
|
|
|
public final class DeviceContactDataManagerImpl: DeviceContactDataManager {
|
|
private let queue = Queue()
|
|
private let impl: QueueLocalObject<DeviceContactDataManagerPrivateImpl>
|
|
|
|
init() {
|
|
let queue = self.queue
|
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
|
return DeviceContactDataManagerPrivateImpl(queue: queue)
|
|
})
|
|
}
|
|
|
|
public func personNameDisplayOrder() -> Signal<PresentationPersonNameOrder, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
disposable.set(impl.personNameDisplayOrder.get().start(next: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
disposable.set(impl.basicData(updated: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber) -> Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
disposable.set(impl.basicDataForNormalizedPhoneNumber(normalizedNumber, updated: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func extendedData(stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
disposable.set(impl.extendedData(stableId: stableId, updated: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func importable() -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
disposable.set(impl.importable(updated: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func appSpecificReferences() -> Signal<[PeerId: DeviceContactBasicDataWithReference], NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
disposable.set(impl.appSpecificReferences(updated: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func search(query: String) -> Signal<[DeviceContactStableId: (DeviceContactBasicData, PeerId?)], NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
disposable.set(impl.search(query: query, updated: { value in
|
|
subscriber.putNext(value)
|
|
subscriber.putCompletion()
|
|
}))
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
impl.appendContactData(contactData, to: stableId, completion: { next in
|
|
subscriber.putNext(next)
|
|
subscriber.putCompletion()
|
|
})
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
impl.appendPhoneNumber(phoneNumber, to: stableId, completion: { next in
|
|
subscriber.putNext(next)
|
|
subscriber.putCompletion()
|
|
})
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
impl.createContactWithData(contactData, completion: { next in
|
|
subscriber.putNext(next)
|
|
subscriber.putCompletion()
|
|
})
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func deleteContactWithAppSpecificReference(peerId: PeerId) -> Signal<Never, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with({ impl in
|
|
impl.deleteContactWithAppSpecificReference(peerId: peerId, completion: {
|
|
subscriber.putCompletion()
|
|
})
|
|
})
|
|
return disposable
|
|
}
|
|
}
|
|
}
|