Spotlight indexing

This commit is contained in:
Ali 2019-12-10 20:14:56 +04:00
parent 4cb77a910d
commit 5d50ba446c
6 changed files with 309 additions and 12 deletions

View File

@ -5143,7 +5143,7 @@ Any member of this group will be able to see messages in the channel.";
"Settings.Devices" = "Devices";
"Settings.AddDevice" = "Scan QR";
"AuthSessions.DevicesTitle" = "Devices";
"AuthSessions.AddDevice" = "Add Device";
"AuthSessions.AddDevice" = "Scan QR";
"AuthSessions.AddDevice.ScanInfo" = "Scan a QR code to log into\nthis account on another device.";
"AuthSessions.AddDevice.ScanTitle" = "Scan QR Code";
"AuthSessions.AddDevice.InvalidQRCode" = "Invalid QR Code";

View File

@ -0,0 +1,19 @@
load("//Config:buck_rule_macros.bzl", "framework")
framework(
name = "SpotlightSupport",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
"//submodules/Postbox:Postbox#shared",
"//submodules/SyncCore:SyncCore#shared",
"//submodules/TelegramCore:TelegramCore#shared",
"//submodules/Display:Display#shared",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/CoreSpotlight.framework",
],
)

View File

@ -35,6 +35,7 @@ import AppLock
import PresentationDataUtils
import TelegramIntents
import AccountUtils
import CoreSpotlight
#if canImport(BackgroundTasks)
import BackgroundTasks
@ -1828,6 +1829,53 @@ final class SharedApplicationContext {
self.openUrl(url: url)
}
if userActivity.activityType == CSSearchableItemActionType {
if let uniqueIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String, uniqueIdentifier.hasPrefix("contact-") {
if let peerIdValue = Int64(String(uniqueIdentifier[uniqueIdentifier.index(uniqueIdentifier.startIndex, offsetBy: "contact-".count)...])) {
let peerId = PeerId(peerIdValue)
let signal = self.sharedContextPromise.get()
|> take(1)
|> mapToSignal { sharedApplicationContext -> Signal<(AccountRecordId?, [Account?]), NoError> in
return sharedApplicationContext.sharedContext.activeAccounts
|> take(1)
|> mapToSignal { primary, accounts, _ -> Signal<(AccountRecordId?, [Account?]), NoError> in
return combineLatest(accounts.map { _, account, _ -> Signal<Account?, NoError> in
return account.postbox.transaction { transaction -> Account? in
if transaction.getPeer(peerId) != nil {
return account
} else {
return nil
}
}
})
|> map { accounts -> (AccountRecordId?, [Account?]) in
return (primary?.id, accounts)
}
}
}
let _ = (signal
|> deliverOnMainQueue).start(next: { primary, accounts in
if let primary = primary {
for account in accounts {
if let account = account, account.id == primary {
self.openChatWhenReady(accountId: nil, peerId: peerId)
return
}
}
}
for account in accounts {
if let account = account {
self.openChatWhenReady(accountId: account.id, peerId: peerId)
return
}
}
})
}
}
}
return true
}

View File

@ -161,6 +161,20 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}
}
self.historyNode.endedInteractiveDragging = { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.historyNode.visibleContentOffset() {
case let .known(value):
if value <= -10.0 {
strongSelf.requestDismiss()
}
default:
break
}
}
self.controlsNode.updateIsExpanded = { [weak self] in
if let strongSelf = self, let validLayout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring))
@ -242,7 +256,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
//self.view.addGestureRecognizer(panRecognizer)
}
func updatePresentationData(_ presentationData: PresentationData) {
@ -305,18 +319,19 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) {
if result == nil {
return self.historyNode.view
}
}
if !self.bounds.contains(point) {
return nil
}
if point.y < self.controlsNode.frame.minY {
return self.dimNode.view
}
let result = super.hitTest(point, with: event)
if self.controlsNode.frame.contains(point) {
// if result == self.historyNode.view {
// return self.view
// }
}
return result
}
@ -532,6 +547,20 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}
}
self.historyNode.endedInteractiveDragging = { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.historyNode.visibleContentOffset() {
case let .known(value):
if value <= -10.0 {
strongSelf.requestDismiss()
}
default:
break
}
}
self.historyNode.beganInteractiveDragging = { [weak self] in
self?.controlsNode.collapse()
}

View File

@ -145,6 +145,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
private let displayUpgradeProgress: (Float?) -> Void
private var spotlightDataContext: SpotlightDataContext?
private var widgetDataContext: WidgetDataContext?
public init(mainWindow: Window1?, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, rootPath: String, legacyBasePath: String?, legacyCache: LegacyCache?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) {
@ -634,10 +635,17 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.updateNotificationTokensRegistration()
self.widgetDataContext = WidgetDataContext(basePath: self.basePath, activeAccount: self.activeAccounts
|> map { primary, _, _ in
return primary
}, presentationData: self.presentationData)
if applicationBindings.isMainApp {
self.widgetDataContext = WidgetDataContext(basePath: self.basePath, activeAccount: self.activeAccounts
|> map { primary, _, _ in
return primary
}, presentationData: self.presentationData)
self.spotlightDataContext = SpotlightDataContext(accounts: self.activeAccounts |> map { _, accounts, _ in
return accounts.map { _, account, _ in
return account
}
})
}
}
deinit {

View File

@ -0,0 +1,193 @@
import Foundation
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 SpotlightAccountContact: Equatable, Codable {
var id: Int64
var title: String
var avatarPath: String?
}
private func manageableSpotlightContacts(accounts: Signal<[Account], NoError>) -> Signal<[Int64: SpotlightAccountContact], NoError> {
let queue = Queue()
return accounts
|> mapToSignal { accounts -> Signal<[[SpotlightAccountContact]], NoError> in
return combineLatest(queue: queue, accounts.map { account -> Signal<[SpotlightAccountContact], NoError> in
return account.postbox.contactPeersView(accountPeerId: account.peerId, includePresences: false)
|> map { view -> [SpotlightAccountContact] in
var result: [SpotlightAccountContact] = []
for peer in view.peers {
if let user = peer as? TelegramUser {
result.append(SpotlightAccountContact(id: user.id.toInt64(), title: user.debugDisplayTitle, avatarPath: smallestImageRepresentation(user.photo).flatMap { representation in
return account.postbox.mediaBox.resourcePath(representation.resource)
}))
}
}
result.sort(by: { $0.id < $1.id })
return result
}
|> distinctUntilChanged
})
}
|> map { accountContacts -> [Int64: SpotlightAccountContact] in
var result: [Int64: SpotlightAccountContact] = [:]
for singleAccountContacts in accountContacts {
for contact in singleAccountContacts {
if result[contact.id] == nil {
result[contact.id] = contact
}
}
}
return result
}
}
private final class SpotlightContactContext {
private let indexQueue: Queue
private let disposable = MetaDisposable()
private var contact: SpotlightAccountContact?
init(indexQueue: Queue) {
self.indexQueue = indexQueue
}
deinit {
self.disposable.dispose()
}
func update(contact: SpotlightAccountContact) {
if self.contact == contact {
return
}
let photoUpdated = self.contact?.avatarPath != contact.avatarPath
self.contact = contact
let indexQueue = self.indexQueue
let indexSignal: Signal<Never, NoError> = Signal { subscriber in
indexQueue.async {
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String)
attributeSet.title = contact.title
if let avatarPath = contact.avatarPath, let avatarData = try? Data(contentsOf: URL(fileURLWithPath: avatarPath)), 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() {
attributeSet.thumbnailData = resultData
}
}
let item = CSSearchableItem(uniqueIdentifier: "contact-\(contact.id)", domainIdentifier: "telegram-contacts", attributeSet: attributeSet)
Logger.shared.log("SpotlightDataContext", "index \(contact.id) title: \(contact.title)")
CSSearchableIndex.default().indexSearchableItems([item], completionHandler: { error in
if let error = error {
Logger.shared.log("CSSearchableIndex", "error: \(error)")
}
subscriber.putCompletion()
})
}
return EmptyDisposable
}
self.disposable.set(indexSignal.start())
}
}
private final class SpotlightDataContextImpl {
private let queue: Queue
private let indexQueue: Queue = Queue()
private var contactContexts: [Int64: SpotlightContactContext] = [:]
private var listDisposable: Disposable?
init(queue: Queue, accounts: Signal<[Account], NoError>) {
self.queue = queue
self.indexQueue.async {
Logger.shared.log("SpotlightDataContext", "deleteSearchableItems")
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["telegram-contacts"], completionHandler: { _ in })
}
self.listDisposable = (manageableSpotlightContacts(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] contacts in
guard let strongSelf = self else {
return
}
strongSelf.updateContacts(contacts: contacts)
})
}
private func updateContacts(contacts: [Int64: SpotlightAccountContact]) {
var validIds = Set<Int64>()
for (_, contact) in contacts {
validIds.insert(contact.id)
let context: SpotlightContactContext
if let current = self.contactContexts[contact.id] {
context = current
} else {
context = SpotlightContactContext(indexQueue: self.indexQueue)
self.contactContexts[contact.id] = context
}
context.update(contact: contact)
}
var removeIds: [Int64] = []
for id in self.contactContexts.keys {
if !validIds.contains(id) {
removeIds.append(id)
}
}
for id in removeIds {
self.contactContexts.removeValue(forKey: id)
}
}
}
public final class SpotlightDataContext {
private let impl: QueueLocalObject<SpotlightDataContextImpl>
public init(accounts: Signal<[Account], NoError>) {
let queue = Queue()
self.impl = QueueLocalObject(queue: queue, generate: {
return SpotlightDataContextImpl(queue: queue, accounts: accounts)
})
}
}