mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Spotlight indexing
This commit is contained in:
parent
4cb77a910d
commit
5d50ba446c
@ -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";
|
||||
|
19
submodules/SpotlightSupport/BUCK
Normal file
19
submodules/SpotlightSupport/BUCK
Normal 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",
|
||||
],
|
||||
)
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
193
submodules/TelegramUI/TelegramUI/SpotlightContacts.swift
Normal file
193
submodules/TelegramUI/TelegramUI/SpotlightContacts.swift
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user