Swiftgram/submodules/TelegramUI/TelegramUI/DeviceContactDataManager.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
}
}
}