Swiftgram/submodules/TelegramUI/Sources/SpotlightContacts.swift
Peter Iakovlev e9a4a9347a Revert "Rename directories [skip ci]"
This reverts commit 789438a27450dcbdee6065ebf096198ed3b90fec
2020-03-01 10:06:51 +00:00

277 lines
12 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import SyncCore
import TelegramCore
import Display
import CoreSpotlight
import MobileCoreServices
private let roundCorners = { () -> UIImage in
let diameter: CGFloat = 60.0
UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter)))
let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
UIGraphicsEndImageContext()
return image
}()
private struct SpotlightIndexStorageItem: Codable, Equatable {
var firstName: String
var lastName: String
var avatarSourcePath: String?
}
private final class SpotlightIndexStorage {
private let appBasePath: String
private let basePath: String
private var items: [PeerId: SpotlightIndexStorageItem] = [:]
init(appBasePath: String, basePath: String) {
self.appBasePath = appBasePath
self.basePath = basePath
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
self.reload()
if self.items.isEmpty {
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["telegram-contacts"], completionHandler: { _ in })
}
}
private func path(peerId: PeerId) -> String {
return self.basePath + "/p:\(UInt64(bitPattern: peerId.toInt64()))"
}
private func reload() {
self.items.removeAll()
guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsSubdirectoryDescendants], errorHandler: nil) else {
return
}
while let item = enumerator.nextObject() {
guard let url = item as? NSURL else {
continue
}
guard let resourceValues = try? url.resourceValues(forKeys: [.isDirectoryKey]) else {
continue
}
if let value = resourceValues[.isDirectoryKey] as? Bool, !value {
continue
}
if let path = url.path, let directoryName = url.lastPathComponent, directoryName.hasPrefix("p:") {
let peerIdString = directoryName[directoryName.index(directoryName.startIndex, offsetBy: 2)...]
if let peerIdValue = UInt64(peerIdString) {
let peerId = PeerId(Int64(bitPattern: peerIdValue))
let item: SpotlightIndexStorageItem
if let itemData = try? Data(contentsOf: URL(fileURLWithPath: path + "/data.json")), let decodedItem = try? JSONDecoder().decode(SpotlightIndexStorageItem.self, from: itemData) {
item = decodedItem
} else {
let _ = try? FileManager.default.removeItem(atPath: path + "/data.json")
let _ = try? FileManager.default.removeItem(atPath: path + "/avatar.png")
item = SpotlightIndexStorageItem(firstName: "", lastName: "", avatarSourcePath: nil)
}
self.items[peerId] = item
}
}
}
}
func update(items: [PeerId: SpotlightIndexStorageItem]) {
let validPeerIds = Set(items.keys)
var removePeerIds: [PeerId] = []
for (peerId, item) in self.items {
if !validPeerIds.contains(peerId) {
removePeerIds.append(peerId)
}
}
if !removePeerIds.isEmpty {
for peerId in removePeerIds {
let _ = try? FileManager.default.removeItem(atPath: self.path(peerId: peerId))
self.items.removeValue(forKey: peerId)
}
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: removePeerIds.map { peerId in
return "contact-\(peerId.toInt64())"
})
}
var addToIndexItems: [CSSearchableItem] = []
for (peerId, item) in items {
let previousItem = self.items[peerId]
if previousItem != item {
var updatedAvatarSourcePath: String?
if let avatarSourcePath = item.avatarSourcePath, let _ = fileSize(self.appBasePath + "/" + avatarSourcePath) {
updatedAvatarSourcePath = avatarSourcePath
}
var encodeItem = item
encodeItem.avatarSourcePath = updatedAvatarSourcePath
if encodeItem == previousItem {
continue
}
print("Spotlight: updating \(item.firstName) \(item.lastName)")
let path = self.path(peerId: peerId)
let _ = try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
var resolvedAvatarPath: String?
if previousItem?.avatarSourcePath != updatedAvatarSourcePath {
let avatarPath = path + "/avatar.png"
let _ = try? FileManager.default.removeItem(atPath: avatarPath)
if let updatedAvatarSourcePathValue = updatedAvatarSourcePath, let avatarData = try? Data(contentsOf: URL(fileURLWithPath: self.appBasePath + "/" + updatedAvatarSourcePathValue)), let image = UIImage(data: avatarData) {
let size = CGSize(width: 120.0, height: 120.0)
let context = DrawingContext(size: size, scale: 1.0, clear: true)
context.withFlippedContext { c in
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
c.setBlendMode(.destinationOut)
c.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: size))
}
if let resultImage = context.generateImage(), let resultData = resultImage.pngData(), let _ = try? resultData.write(to: URL(fileURLWithPath: avatarPath)) {
resolvedAvatarPath = avatarPath
} else {
updatedAvatarSourcePath = nil
}
}
}
let itemDataPath = path + "/data.json"
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String)
attributeSet.version = "\(UInt64.random(in: 0 ..< UInt64.max))"
if !item.firstName.isEmpty && !item.lastName.isEmpty {
attributeSet.title = "\(item.firstName) \(item.lastName)"
} else if !item.firstName.isEmpty {
attributeSet.title = item.firstName
} else {
attributeSet.title = item.lastName
}
attributeSet.thumbnailURL = resolvedAvatarPath.flatMap(URL.init(fileURLWithPath:))
let indexItem = CSSearchableItem(uniqueIdentifier: "contact-\(peerId.toInt64())", domainIdentifier: "telegram-contacts", attributeSet: attributeSet)
addToIndexItems.append(indexItem)
encodeItem.avatarSourcePath = updatedAvatarSourcePath
if let data = try? JSONEncoder().encode(encodeItem) {
let _ = try? data.write(to: URL(fileURLWithPath: itemDataPath), options: [.atomic])
}
self.items[peerId] = item
}
}
if !addToIndexItems.isEmpty {
CSSearchableIndex.default().indexSearchableItems(addToIndexItems, completionHandler: { error in
if let error = error {
Logger.shared.log("CSSearchableIndex", "indexSearchableItems error: \(error)")
}
})
}
}
}
private func manageableSpotlightContacts(appBasePath: String, accounts: Signal<[Account], NoError>) -> Signal<[PeerId: SpotlightIndexStorageItem], NoError> {
let queue = Queue()
return accounts
|> mapToSignal { accounts -> Signal<[[PeerId: SpotlightIndexStorageItem]], NoError> in
return combineLatest(queue: queue, accounts.map { account -> Signal<[PeerId: SpotlightIndexStorageItem], NoError> in
return account.postbox.contactPeersView(accountPeerId: account.peerId, includePresences: false)
|> map { view -> [PeerId: SpotlightIndexStorageItem] in
var result: [PeerId: SpotlightIndexStorageItem] = [:]
for peer in view.peers {
if let user = peer as? TelegramUser {
let avatarSourcePath = smallestImageRepresentation(user.photo).flatMap { representation -> String? in
let resourcePath = account.postbox.mediaBox.resourcePath(representation.resource)
if resourcePath.hasPrefix(appBasePath + "/") {
return String(resourcePath[resourcePath.index(resourcePath.startIndex, offsetBy: appBasePath.count + 1)...])
} else {
return resourcePath
}
}
result[user.id] = SpotlightIndexStorageItem(firstName: user.firstName ?? "", lastName: user.lastName ?? "", avatarSourcePath: avatarSourcePath)
}
}
return result
}
|> distinctUntilChanged
})
}
|> map { accountContacts -> [PeerId: SpotlightIndexStorageItem] in
var result: [PeerId: SpotlightIndexStorageItem] = [:]
for singleAccountContacts in accountContacts {
for (peerId, contact) in singleAccountContacts {
if result[peerId] == nil {
result[peerId] = contact
}
}
}
return result
}
}
private final class SpotlightDataContextImpl {
private let queue: Queue
private let appBasePath: String
private let accountManager: AccountManager
private let indexStorage: SpotlightIndexStorage
private var listDisposable: Disposable?
init(queue: Queue, appBasePath: String, accountManager: AccountManager, accounts: Signal<[Account], NoError>) {
self.queue = queue
self.appBasePath = appBasePath
self.accountManager = accountManager
self.indexStorage = SpotlightIndexStorage(appBasePath: appBasePath, basePath: accountManager.basePath + "/spotlight")
self.listDisposable = (manageableSpotlightContacts(appBasePath: appBasePath, accounts: accounts
|> map { accounts in
return accounts.sorted(by: { $0.id < $1.id })
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs.count != rhs.count {
return false
}
for i in 0 ..< lhs.count {
if lhs[i] !== rhs[i] {
return false
}
}
return true
}))
|> deliverOn(self.queue)).start(next: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.updateContacts(items: items)
})
}
private func updateContacts(items: [PeerId: SpotlightIndexStorageItem]) {
self.indexStorage.update(items: items)
}
}
public final class SpotlightDataContext {
private let impl: QueueLocalObject<SpotlightDataContextImpl>
public init(appBasePath: String, accountManager: AccountManager, accounts: Signal<[Account], NoError>) {
let queue = Queue()
self.impl = QueueLocalObject(queue: queue, generate: {
return SpotlightDataContextImpl(queue: queue, appBasePath: appBasePath, accountManager: accountManager, accounts: accounts)
})
}
}