mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
277 lines
13 KiB
Swift
277 lines
13 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import SwiftSignalKit
|
|
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: [EnginePeer.Id: 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: EnginePeer.Id) -> 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 = EnginePeer.Id(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: [EnginePeer.Id: SpotlightIndexStorageItem]) {
|
|
let validPeerIds = Set(items.keys)
|
|
var removePeerIds: [EnginePeer.Id] = []
|
|
for (peerId, _) 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, FileManager.default.fileExists(atPath: 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)
|
|
if 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<[EnginePeer.Id: SpotlightIndexStorageItem], NoError> {
|
|
let queue = Queue()
|
|
return accounts
|
|
|> mapToSignal { accounts -> Signal<[[EnginePeer.Id: SpotlightIndexStorageItem]], NoError> in
|
|
return combineLatest(queue: queue, accounts.map { account -> Signal<[EnginePeer.Id: SpotlightIndexStorageItem], NoError> in
|
|
return TelegramEngine(account: account).data.subscribe(
|
|
TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)
|
|
)
|
|
|> map { view -> [EnginePeer.Id: SpotlightIndexStorageItem] in
|
|
var result: [EnginePeer.Id: SpotlightIndexStorageItem] = [:]
|
|
for peer in view.peers {
|
|
if case let .user(user) = peer {
|
|
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 -> [EnginePeer.Id: SpotlightIndexStorageItem] in
|
|
var result: [EnginePeer.Id: 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<TelegramAccountManagerTypes>
|
|
private let indexStorage: SpotlightIndexStorage
|
|
|
|
private var listDisposable: Disposable?
|
|
|
|
init(queue: Queue, appBasePath: String, accountManager: AccountManager<TelegramAccountManagerTypes>, 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: [EnginePeer.Id: SpotlightIndexStorageItem]) {
|
|
self.indexStorage.update(items: items)
|
|
}
|
|
}
|
|
|
|
public final class SpotlightDataContext {
|
|
private let impl: QueueLocalObject<SpotlightDataContextImpl>
|
|
|
|
public init(appBasePath: String, accountManager: AccountManager<TelegramAccountManagerTypes>, accounts: Signal<[Account], NoError>) {
|
|
let queue = Queue()
|
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
|
return SpotlightDataContextImpl(queue: queue, appBasePath: appBasePath, accountManager: accountManager, accounts: accounts)
|
|
})
|
|
}
|
|
}
|