mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-10 23:12:55 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
3048 lines
148 KiB
Swift
3048 lines
148 KiB
Swift
// MARK: Swiftgram
|
|
import SGActionRequestHandlerSanitizer
|
|
import SGAPIWebSettings
|
|
import SGLogging
|
|
import SGStrings
|
|
import UIKit
|
|
import SwiftSignalKit
|
|
import Display
|
|
import TelegramCore
|
|
import UserNotifications
|
|
import Intents
|
|
import Postbox
|
|
import PushKit
|
|
import AsyncDisplayKit
|
|
import TelegramUIPreferences
|
|
import TelegramPresentationData
|
|
import TelegramCallsUI
|
|
import TelegramVoip
|
|
import BuildConfig
|
|
import BuildConfigExtra
|
|
import DeviceCheck
|
|
import AccountContext
|
|
import OverlayStatusController
|
|
import UndoUI
|
|
import LegacyUI
|
|
import PassportUI
|
|
import WatchBridge
|
|
import SettingsUI
|
|
import AppBundle
|
|
import UrlHandling
|
|
import OpenSSLEncryptionProvider
|
|
import AppLock
|
|
import PresentationDataUtils
|
|
import TelegramIntents
|
|
import AccountUtils
|
|
import CoreSpotlight
|
|
import TelegramAudio
|
|
import DebugSettingsUI
|
|
import BackgroundTasks
|
|
import UIKitRuntimeUtils
|
|
import StoreKit
|
|
import PhoneNumberFormat
|
|
import AuthorizationUI
|
|
import ManagedFile
|
|
import DeviceProximity
|
|
import MediaEditor
|
|
import TelegramUIDeclareEncodables
|
|
import ContextMenuScreen
|
|
import MetalEngine
|
|
|
|
#if canImport(AppCenter)
|
|
import AppCenter
|
|
import AppCenterCrashes
|
|
#endif
|
|
|
|
private let handleVoipNotifications = false
|
|
|
|
private var testIsLaunched = false
|
|
|
|
private func isKeyboardWindow(window: NSObject) -> Bool {
|
|
let typeName = NSStringFromClass(type(of: window))
|
|
if #available(iOS 9.0, *) {
|
|
if typeName.hasPrefix("UI") && typeName.hasSuffix("RemoteKeyboardWindow") {
|
|
return true
|
|
}
|
|
} else {
|
|
if typeName.hasPrefix("UI") && typeName.hasSuffix("TextEffectsWindow") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func isKeyboardView(view: NSObject) -> Bool {
|
|
let typeName = NSStringFromClass(type(of: view))
|
|
if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetHostView") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func isKeyboardViewContainer(view: NSObject) -> Bool {
|
|
let typeName = NSStringFromClass(type(of: view))
|
|
if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetContainerView") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private class ApplicationStatusBarHost: StatusBarHost {
|
|
private let application = UIApplication.shared
|
|
|
|
var isApplicationInForeground: Bool {
|
|
switch self.application.applicationState {
|
|
case .background:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
var statusBarFrame: CGRect {
|
|
return self.application.statusBarFrame
|
|
}
|
|
var statusBarStyle: UIStatusBarStyle {
|
|
get {
|
|
return self.application.statusBarStyle
|
|
} set(value) {
|
|
self.setStatusBarStyle(value, animated: false)
|
|
}
|
|
}
|
|
|
|
func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) {
|
|
if self.shouldChangeStatusBarStyle?(style) ?? true {
|
|
self.application.internalSetStatusBarStyle(style, animated: animated)
|
|
}
|
|
}
|
|
|
|
var shouldChangeStatusBarStyle: ((UIStatusBarStyle) -> Bool)?
|
|
|
|
func setStatusBarHidden(_ value: Bool, animated: Bool) {
|
|
self.application.internalSetStatusBarHidden(value, animation: animated ? .fade : .none)
|
|
}
|
|
|
|
var keyboardWindow: UIWindow? {
|
|
if #available(iOS 16.0, *) {
|
|
return UIApplication.shared.internalGetKeyboard()
|
|
}
|
|
|
|
for window in UIApplication.shared.windows {
|
|
if isKeyboardWindow(window: window) {
|
|
return window
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var keyboardView: UIView? {
|
|
guard let keyboardWindow = self.keyboardWindow else {
|
|
return nil
|
|
}
|
|
|
|
for view in keyboardWindow.subviews {
|
|
if isKeyboardViewContainer(view: view) {
|
|
for subview in view.subviews {
|
|
if isKeyboardView(view: subview) {
|
|
return subview
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func legacyDocumentsPath() -> String {
|
|
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/legacy"
|
|
}
|
|
|
|
protocol SupportedStartCallIntent {
|
|
@available(iOS 10.0, *)
|
|
var contacts: [INPerson]? { get }
|
|
}
|
|
|
|
@available(iOS 10.0, *)
|
|
extension INStartAudioCallIntent: SupportedStartCallIntent {}
|
|
|
|
protocol SupportedStartVideoCallIntent {
|
|
@available(iOS 10.0, *)
|
|
var contacts: [INPerson]? { get }
|
|
}
|
|
|
|
@available(iOS 10.0, *)
|
|
extension INStartVideoCallIntent: SupportedStartVideoCallIntent {}
|
|
|
|
private enum QueuedWakeup: Int32 {
|
|
case call
|
|
case backgroundLocation
|
|
}
|
|
|
|
final class SharedApplicationContext {
|
|
let sharedContext: SharedAccountContextImpl
|
|
let notificationManager: SharedNotificationManager
|
|
let wakeupManager: SharedWakeupManager
|
|
let overlayMediaController: ViewController & OverlayMediaController
|
|
var minimizedContainer: [AccountRecordId: MinimizedContainer] = [:]
|
|
|
|
init(sharedContext: SharedAccountContextImpl, notificationManager: SharedNotificationManager, wakeupManager: SharedWakeupManager) {
|
|
self.sharedContext = sharedContext
|
|
self.notificationManager = notificationManager
|
|
self.wakeupManager = wakeupManager
|
|
self.overlayMediaController = OverlayMediaControllerImpl()
|
|
}
|
|
}
|
|
|
|
private struct AccountManagerState {
|
|
struct NotificationKey {
|
|
var accountId: AccountRecordId
|
|
var id: Data
|
|
var key: Data
|
|
}
|
|
|
|
var notificationKeys: [NotificationKey]
|
|
}
|
|
|
|
private func extractAccountManagerState(records: AccountRecordsView<TelegramAccountManagerTypes>) -> AccountManagerState {
|
|
return AccountManagerState(
|
|
notificationKeys: records.records.compactMap { record -> AccountManagerState.NotificationKey? in
|
|
for attribute in record.attributes {
|
|
if case let .backupData(backupData) = attribute {
|
|
if let notificationEncryptionKeyId = backupData.data?.notificationEncryptionKeyId, let notificationEncryptionKey = backupData.data?.notificationEncryptionKey {
|
|
return AccountManagerState.NotificationKey(
|
|
accountId: record.id,
|
|
id: notificationEncryptionKeyId,
|
|
key: notificationEncryptionKey
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
)
|
|
}
|
|
|
|
@objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate, URLSessionDelegate, URLSessionTaskDelegate {
|
|
@objc var window: UIWindow?
|
|
var nativeWindow: (UIWindow & WindowHost)?
|
|
var mainWindow: Window1!
|
|
private var dataImportSplash: LegacyDataImportSplash?
|
|
private var memoryUsageOverlayView: UILabel?
|
|
|
|
private var buildConfig: BuildConfig?
|
|
let episodeId = arc4random()
|
|
|
|
private let isInForegroundPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
private var isInForegroundValue = false
|
|
private let isActivePromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
private var isActiveValue = false
|
|
let hasActiveAudioSession = Promise<Bool>(false)
|
|
|
|
private let sharedContextPromise = Promise<SharedApplicationContext>()
|
|
//private let watchCommunicationManagerPromise = Promise<WatchCommunicationManager?>()
|
|
|
|
private var accountManager: AccountManager<TelegramAccountManagerTypes>?
|
|
private var accountManagerState: AccountManagerState?
|
|
|
|
private var contextValue: AuthorizedApplicationContext?
|
|
private let context = Promise<AuthorizedApplicationContext?>()
|
|
private let contextDisposable = MetaDisposable()
|
|
|
|
private var authContextValue: UnauthorizedApplicationContext?
|
|
private let authContext = Promise<UnauthorizedApplicationContext?>()
|
|
private let authContextDisposable = MetaDisposable()
|
|
|
|
private let logoutDisposable = MetaDisposable()
|
|
|
|
private let openNotificationSettingsWhenReadyDisposable = MetaDisposable()
|
|
private let openChatWhenReadyDisposable = MetaDisposable()
|
|
private let openUrlWhenReadyDisposable = MetaDisposable()
|
|
|
|
private let badgeDisposable = MetaDisposable()
|
|
private let quickActionsDisposable = MetaDisposable()
|
|
|
|
private var pushRegistry: PKPushRegistry?
|
|
|
|
private let notificationAuthorizationDisposable = MetaDisposable()
|
|
|
|
private var replyFromNotificationsDisposables = DisposableSet()
|
|
private var watchedCallsDisposables = DisposableSet()
|
|
|
|
private var _notificationTokenPromise: Promise<Data>?
|
|
private let voipTokenPromise = Promise<Data>()
|
|
|
|
private var firebaseSecrets: [String: String] = [:] {
|
|
didSet {
|
|
if self.firebaseSecrets != oldValue {
|
|
self.firebaseSecretStream.set(.single(self.firebaseSecrets))
|
|
}
|
|
}
|
|
}
|
|
private let firebaseSecretStream = Promise<[String: String]>([:])
|
|
|
|
private var firebaseRequestVerificationSecrets: [String: String] = [:] {
|
|
didSet {
|
|
if self.firebaseRequestVerificationSecrets != oldValue {
|
|
self.firebaseRequestVerificationSecretStream.set(.single(self.firebaseRequestVerificationSecrets))
|
|
}
|
|
}
|
|
}
|
|
private let firebaseRequestVerificationSecretStream = Promise<[String: String]>([:])
|
|
|
|
private var urlSessions: [URLSession] = []
|
|
private func urlSession(identifier: String) -> URLSession {
|
|
if let existingSession = self.urlSessions.first(where: { $0.configuration.identifier == identifier }) {
|
|
return existingSession
|
|
}
|
|
|
|
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
|
let appGroupName = "group.\(baseAppBundleId)"
|
|
|
|
let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
|
|
configuration.sharedContainerIdentifier = appGroupName
|
|
configuration.isDiscretionary = false
|
|
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
|
|
self.urlSessions.append(session)
|
|
return session
|
|
}
|
|
|
|
private var pendingUrlSessionBackgroundEventsCompletion: (() -> Void)?
|
|
|
|
private var notificationTokenPromise: Promise<Data> {
|
|
if let current = self._notificationTokenPromise {
|
|
return current
|
|
} else {
|
|
let promise = Promise<Data>()
|
|
self._notificationTokenPromise = promise
|
|
|
|
return promise
|
|
}
|
|
}
|
|
|
|
private var clearNotificationsManager: ClearNotificationsManager?
|
|
|
|
private let idleTimerExtensionSubscribers = Bag<Void>()
|
|
|
|
private var alertActions: (primary: (() -> Void)?, other: (() -> Void)?)?
|
|
|
|
private let voipDeviceToken = Promise<Data?>(nil)
|
|
private let regularDeviceToken = Promise<Data?>(nil)
|
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
precondition(!testIsLaunched)
|
|
testIsLaunched = true
|
|
|
|
let _ = voipTokenPromise.get().start(next: { token in
|
|
self.voipDeviceToken.set(.single(token))
|
|
})
|
|
let _ = notificationTokenPromise.get().start(next: { token in
|
|
self.regularDeviceToken.set(.single(token))
|
|
})
|
|
|
|
let launchStartTime = CFAbsoluteTimeGetCurrent()
|
|
|
|
let statusBarHost = ApplicationStatusBarHost()
|
|
let (window, hostView) = nativeWindowHostView()
|
|
self.mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost)
|
|
if let traitCollection = window.rootViewController?.traitCollection {
|
|
if #available(iOS 13.0, *) {
|
|
switch traitCollection.userInterfaceStyle {
|
|
case .light, .unspecified:
|
|
hostView.containerView.backgroundColor = UIColor.white
|
|
default:
|
|
hostView.containerView.backgroundColor = UIColor.black
|
|
}
|
|
} else {
|
|
hostView.containerView.backgroundColor = UIColor.white
|
|
}
|
|
} else {
|
|
hostView.containerView.backgroundColor = UIColor.white
|
|
}
|
|
self.window = window
|
|
self.nativeWindow = window
|
|
|
|
hostView.containerView.layer.addSublayer(MetalEngine.shared.rootLayer)
|
|
|
|
if !UIDevice.current.isBatteryMonitoringEnabled {
|
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
}
|
|
|
|
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
|
|
if #available(iOS 10.0, *) {
|
|
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
|
|
var result: [(String, NotificationManagedNotificationRequestId)] = []
|
|
for notification in notifications {
|
|
if let requestId = NotificationManagedNotificationRequestId(string: notification.request.identifier) {
|
|
result.append((notification.request.identifier, requestId))
|
|
} else {
|
|
let payload = notification.request.content.userInfo
|
|
var notificationRequestId: NotificationManagedNotificationRequestId?
|
|
|
|
var peerId: PeerId?
|
|
if let fromId = payload["from_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
|
|
} else if let fromId = payload["chat_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
|
|
} else if let fromId = payload["channel_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
|
|
}
|
|
|
|
if let msgId = payload["msg_id"] {
|
|
let msgIdValue = msgId as! NSString
|
|
if let peerId = peerId {
|
|
notificationRequestId = .messageId(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue)))
|
|
}
|
|
}
|
|
|
|
if let notificationRequestId = notificationRequestId {
|
|
result.append((notification.request.identifier, notificationRequestId))
|
|
}
|
|
}
|
|
}
|
|
completion.f(result)
|
|
})
|
|
} else {
|
|
var result: [(String, NotificationManagedNotificationRequestId)] = []
|
|
if let notifications = UIApplication.shared.scheduledLocalNotifications {
|
|
for notification in notifications {
|
|
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
|
|
if let requestId = NotificationManagedNotificationRequestId(string: id) {
|
|
result.append((id, requestId))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
completion.f(result)
|
|
}
|
|
}, removeNotificationIds: { ids in
|
|
if #available(iOS 10.0, *) {
|
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
|
|
} else {
|
|
if let notifications = UIApplication.shared.scheduledLocalNotifications {
|
|
for notification in notifications {
|
|
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
|
|
if ids.contains(id) {
|
|
UIApplication.shared.cancelLocalNotification(notification)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, getPendingNotificationIds: { completion in
|
|
if #available(iOS 10.0, *) {
|
|
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { requests in
|
|
var result: [(String, NotificationManagedNotificationRequestId)] = []
|
|
for request in requests {
|
|
if let requestId = NotificationManagedNotificationRequestId(string: request.identifier) {
|
|
result.append((request.identifier, requestId))
|
|
}
|
|
}
|
|
completion.f(result)
|
|
})
|
|
} else {
|
|
var result: [(String, NotificationManagedNotificationRequestId)] = []
|
|
if let notifications = UIApplication.shared.scheduledLocalNotifications {
|
|
for notification in notifications {
|
|
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
|
|
if let requestId = NotificationManagedNotificationRequestId(string: id) {
|
|
result.append((id, requestId))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
completion.f(result)
|
|
}
|
|
}, removePendingNotificationIds: { ids in
|
|
if #available(iOS 10.0, *) {
|
|
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
|
|
} else {
|
|
if let notifications = UIApplication.shared.scheduledLocalNotifications {
|
|
for notification in notifications {
|
|
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
|
|
if ids.contains(id) {
|
|
UIApplication.shared.cancelLocalNotification(notification)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
self.clearNotificationsManager = clearNotificationsManager
|
|
|
|
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
|
|
|
|
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
|
let appGroupName = "group.\(baseAppBundleId)"
|
|
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
|
|
|
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
|
self.buildConfig = buildConfig
|
|
let signatureDict = BuildConfigExtra.signatureDict()
|
|
|
|
let apiId: Int32 = buildConfig.apiId
|
|
let apiHash: String = buildConfig.apiHash
|
|
let languagesCategory = "ios"
|
|
|
|
let autolockDeadine: Signal<Int32?, NoError>
|
|
if #available(iOS 10.0, *) {
|
|
autolockDeadine = .single(nil)
|
|
} else {
|
|
autolockDeadine = self.context.get()
|
|
|> mapToSignal { context -> Signal<Int32?, NoError> in
|
|
guard let context = context else {
|
|
return .single(nil)
|
|
}
|
|
return context.context.sharedContext.appLockContext.autolockDeadline
|
|
}
|
|
}
|
|
|
|
let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: PresentationCallManagerImpl.voipMaxLayer, voipVersions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: false).map { version, supportsVideo -> CallSessionManagerImplementationVersion in
|
|
CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo)
|
|
}, appData: self.regularDeviceToken.get()
|
|
|> map { token in
|
|
let tokenEnvironment: String
|
|
#if DEBUG
|
|
tokenEnvironment = "sandbox"
|
|
#else
|
|
tokenEnvironment = "production"
|
|
#endif
|
|
|
|
let data = buildConfig.bundleData(withAppToken: token, tokenType: "apns", tokenEnvironment: tokenEnvironment, signatureDict: signatureDict)
|
|
if let data = data, let _ = String(data: data, encoding: .utf8) {
|
|
} else {
|
|
Logger.shared.log("data", "can't deserialize")
|
|
}
|
|
return data
|
|
}, externalRequestVerificationStream: self.firebaseRequestVerificationSecretStream.get(), autolockDeadine: autolockDeadine, encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: buildConfig.isICloudEnabled)
|
|
|
|
guard let appGroupUrl = maybeAppGroupUrl else {
|
|
self.mainWindow?.presentNative(UIAlertController(title: nil, message: "Error 2", preferredStyle: .alert))
|
|
return true
|
|
}
|
|
|
|
var isDebugConfiguration = false
|
|
#if DEBUG
|
|
isDebugConfiguration = true
|
|
#endif
|
|
|
|
if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" {
|
|
isDebugConfiguration = true
|
|
}
|
|
|
|
if isDebugConfiguration || buildConfig.isInternalBuild {
|
|
LoggingSettings.defaultSettings = LoggingSettings(logToFile: true, logToConsole: false, redactSensitiveData: true)
|
|
} else {
|
|
LoggingSettings.defaultSettings = LoggingSettings(logToFile: false, logToConsole: false, redactSensitiveData: true)
|
|
}
|
|
|
|
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
|
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
|
|
|
|
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
|
|
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
|
|
|
|
TempBox.initializeShared(basePath: rootPath, processType: "app", launchSpecificId: Int64.random(in: Int64.min ... Int64.max))
|
|
|
|
let writeAbilityTestFile = TempBox.shared.tempFile(fileName: "test.bin")
|
|
var writeAbilityTestSuccess = true
|
|
if let testFile = ManagedFile(queue: nil, path: writeAbilityTestFile.path, mode: .readwrite) {
|
|
let bufferSize = 128 * 1024
|
|
let randomBuffer = malloc(bufferSize)!
|
|
defer {
|
|
free(randomBuffer)
|
|
}
|
|
arc4random_buf(randomBuffer, bufferSize)
|
|
var writtenBytes = 0
|
|
while writtenBytes < 1024 * 1024 {
|
|
let actualBytes = testFile.write(randomBuffer, count: bufferSize)
|
|
writtenBytes += actualBytes
|
|
if actualBytes != bufferSize {
|
|
writeAbilityTestSuccess = false
|
|
break
|
|
}
|
|
}
|
|
testFile._unsafeClose()
|
|
TempBox.shared.dispose(writeAbilityTestFile)
|
|
} else {
|
|
writeAbilityTestSuccess = false
|
|
}
|
|
|
|
if !writeAbilityTestSuccess {
|
|
let alertController = UIAlertController(title: nil, message: "The device does not have sufficient free space.", preferredStyle: .alert)
|
|
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
|
|
preconditionFailure()
|
|
}))
|
|
self.mainWindow?.presentNative(alertController)
|
|
|
|
return true
|
|
}
|
|
|
|
let legacyLogs: [String] = [
|
|
"broadcast-logs",
|
|
"siri-logs",
|
|
"widget-logs",
|
|
"notificationcontent-logs",
|
|
"notification-logs"
|
|
]
|
|
for item in legacyLogs {
|
|
let _ = try? FileManager.default.removeItem(atPath: "\(rootPath)/\(item)")
|
|
}
|
|
|
|
let logsPath = rootPath + "/logs/app-logs"
|
|
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
|
Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: logsPath))
|
|
|
|
setManagedAudioSessionLogger({ s in
|
|
Logger.shared.log("ManagedAudioSession", s)
|
|
Logger.shared.shortLog("ManagedAudioSession", s)
|
|
})
|
|
|
|
if let contents = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath + "/accounts-metadata"), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) {
|
|
for url in contents {
|
|
Logger.shared.log("App \(self.episodeId)", "metadata: \(url.path)")
|
|
}
|
|
}
|
|
|
|
if let contents = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) {
|
|
for url in contents {
|
|
Logger.shared.log("App \(self.episodeId)", "root: \(url.path)")
|
|
if url.lastPathComponent.hasPrefix("account-") {
|
|
if let subcontents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) {
|
|
for suburl in subcontents {
|
|
Logger.shared.log("App \(self.episodeId)", "account \(url.lastPathComponent): \(suburl.path)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//ASDisableLogging()
|
|
|
|
initializeLegacyComponents(application: application, currentSizeClassGetter: {
|
|
return UIUserInterfaceSizeClass.compact
|
|
}, currentHorizontalClassGetter: {
|
|
return UIUserInterfaceSizeClass.compact
|
|
}, documentsPath: legacyDocumentsPath(), currentApplicationBounds: {
|
|
return UIScreen.main.bounds
|
|
}, canOpenUrl: { url in
|
|
return UIApplication.shared.canOpenURL(url)
|
|
}, openUrl: { url in
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
})
|
|
setContextMenuControllerProvider { arguments in
|
|
return ContextMenuControllerImpl(arguments)
|
|
}
|
|
|
|
if #available(iOS 10.0, *) {
|
|
UNUserNotificationCenter.current().delegate = self
|
|
}
|
|
|
|
GlobalExperimentalSettings.isAppStoreBuild = buildConfig.isAppStoreBuild
|
|
GlobalExperimentalSettings.enableFeed = false
|
|
|
|
self.window?.makeKeyAndVisible()
|
|
|
|
var hasActiveCalls: Signal<Bool, NoError> = .single(false)
|
|
if CallKitIntegration.isAvailable, let callKitIntegration = CallKitIntegration.shared {
|
|
hasActiveCalls = callKitIntegration.hasActiveCalls
|
|
}
|
|
self.hasActiveAudioSession.set(
|
|
combineLatest(queue: .mainQueue(),
|
|
hasActiveCalls,
|
|
MediaManagerImpl.globalAudioSession.isActive()
|
|
)
|
|
|> map { hasActiveCalls, isActive -> Bool in
|
|
return hasActiveCalls || isActive
|
|
}
|
|
|> distinctUntilChanged
|
|
)
|
|
|
|
let applicationBindings = TelegramApplicationBindings(isMainApp: true, appBundleId: baseAppBundleId, appBuildType: buildConfig.isAppStoreBuild ? .public : .internal, containerPath: appGroupUrl.path, appSpecificScheme: buildConfig.appSpecificUrlScheme, openUrl: { url in
|
|
var parsedUrl = URL(string: url)
|
|
if let parsed = parsedUrl {
|
|
if parsed.scheme == nil || parsed.scheme!.isEmpty {
|
|
parsedUrl = URL(string: "https://\(url)")
|
|
}
|
|
if parsed.scheme == "tg" {
|
|
return
|
|
}
|
|
}
|
|
|
|
if let parsedUrl = parsedUrl {
|
|
UIApplication.shared.open(parsedUrl, options: [:], completionHandler: nil)
|
|
} else if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) {
|
|
UIApplication.shared.open(parsedUrl, options: [:], completionHandler: nil)
|
|
}
|
|
}, openUniversalUrl: { url, completion in
|
|
if #available(iOS 10.0, *) {
|
|
var parsedUrl = URL(string: url)
|
|
if let parsed = parsedUrl {
|
|
if parsed.scheme == nil || parsed.scheme!.isEmpty {
|
|
parsedUrl = URL(string: "https://\(url)")
|
|
}
|
|
}
|
|
|
|
if let parsedUrl = parsedUrl {
|
|
return UIApplication.shared.open(parsedUrl, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: true as NSNumber], completionHandler: { value in
|
|
completion.completion(value)
|
|
})
|
|
} else if let escapedUrl = (url.removingPercentEncoding ?? url).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) {
|
|
return UIApplication.shared.open(parsedUrl, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: true as NSNumber], completionHandler: { value in
|
|
completion.completion(value)
|
|
})
|
|
} else {
|
|
completion.completion(false)
|
|
}
|
|
} else {
|
|
completion.completion(false)
|
|
}
|
|
}, canOpenUrl: { url in
|
|
var parsedUrl = URL(string: url)
|
|
if let parsed = parsedUrl {
|
|
if parsed.scheme == nil || parsed.scheme!.isEmpty {
|
|
parsedUrl = URL(string: "https://\(url)")
|
|
}
|
|
}
|
|
if let parsedUrl = parsedUrl {
|
|
return UIApplication.shared.canOpenURL(parsedUrl)
|
|
} else if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) {
|
|
return UIApplication.shared.canOpenURL(parsedUrl)
|
|
} else {
|
|
return false
|
|
}
|
|
}, getTopWindow: {
|
|
for window in application.windows.reversed() {
|
|
if window === self.window || window === statusBarHost.keyboardWindow {
|
|
return window
|
|
}
|
|
}
|
|
return application.windows.last
|
|
}, displayNotification: { text in
|
|
}, applicationInForeground: self.isInForegroundPromise.get(),
|
|
applicationIsActive: self.isActivePromise.get(),
|
|
clearMessageNotifications: { ids in
|
|
for id in ids {
|
|
self.clearNotificationsManager?.append(id)
|
|
}
|
|
}, pushIdleTimerExtension: {
|
|
let disposable = MetaDisposable()
|
|
Queue.mainQueue().async {
|
|
let wasEmpty = self.idleTimerExtensionSubscribers.isEmpty
|
|
let index = self.idleTimerExtensionSubscribers.add(Void())
|
|
|
|
if wasEmpty {
|
|
application.isIdleTimerDisabled = true
|
|
}
|
|
|
|
disposable.set(ActionDisposable {
|
|
Queue.mainQueue().async {
|
|
self.idleTimerExtensionSubscribers.remove(index)
|
|
if self.idleTimerExtensionSubscribers.isEmpty {
|
|
application.isIdleTimerDisabled = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
return disposable
|
|
}, openSettings: {
|
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
}, openAppStorePage: {
|
|
let appStoreId = buildConfig.appStoreId
|
|
if let url = URL(string: "itms-apps://itunes.apple.com/app/id\(appStoreId)") {
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
}, openSubscriptions: {
|
|
if #available(iOS 15, *), let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
|
Task {
|
|
try await AppStore.showManageSubscriptions(in: scene)
|
|
}
|
|
} else if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
}, registerForNotifications: { completion in
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications begin")
|
|
let _ = (self.context.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
if let context = context {
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications initiate")
|
|
self.registerForNotifications(context: context.context, authorize: true, completion: completion)
|
|
}
|
|
})
|
|
}, requestSiriAuthorization: { completion in
|
|
if #available(iOS 10, *) {
|
|
INPreferences.requestSiriAuthorization { status in
|
|
if case .authorized = status {
|
|
completion(true)
|
|
} else {
|
|
completion(false)
|
|
}
|
|
}
|
|
} else {
|
|
completion(false)
|
|
}
|
|
}, siriAuthorization: {
|
|
if buildConfig.isSiriEnabled {
|
|
if #available(iOS 10, *) {
|
|
switch INPreferences.siriAuthorizationStatus() {
|
|
case .authorized:
|
|
return .allowed
|
|
case .denied, .restricted:
|
|
return .denied
|
|
case .notDetermined:
|
|
return .notDetermined
|
|
@unknown default:
|
|
return .notDetermined
|
|
}
|
|
} else {
|
|
return .denied
|
|
}
|
|
} else {
|
|
return .denied
|
|
}
|
|
}, getWindowHost: {
|
|
return self.nativeWindow
|
|
}, presentNativeController: { controller in
|
|
self.window?.rootViewController?.present(controller, animated: true, completion: nil)
|
|
}, dismissNativeController: {
|
|
self.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
|
}, getAvailableAlternateIcons: {
|
|
if #available(iOS 10.3, *) {
|
|
var icons = [
|
|
PresentationAppIcon(name: "BlueIcon", imageName: "BlueIcon", isDefault: buildConfig.isAppStoreBuild),
|
|
PresentationAppIcon(name: "New2", imageName: "New2"),
|
|
PresentationAppIcon(name: "New1", imageName: "New1"),
|
|
PresentationAppIcon(name: "BlackIcon", imageName: "BlackIcon"),
|
|
PresentationAppIcon(name: "BlueClassicIcon", imageName: "BlueClassicIcon"),
|
|
PresentationAppIcon(name: "BlackClassicIcon", imageName: "BlackClassicIcon"),
|
|
PresentationAppIcon(name: "BlueFilledIcon", imageName: "BlueFilledIcon"),
|
|
PresentationAppIcon(name: "BlackFilledIcon", imageName: "BlackFilledIcon")
|
|
]
|
|
if buildConfig.isInternalBuild {
|
|
icons.append(PresentationAppIcon(name: "WhiteFilledIcon", imageName: "WhiteFilledIcon"))
|
|
}
|
|
|
|
icons.append(PresentationAppIcon(name: "Premium", imageName: "Premium", isPremium: true))
|
|
icons.append(PresentationAppIcon(name: "PremiumTurbo", imageName: "PremiumTurbo", isPremium: true))
|
|
icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true))
|
|
|
|
|
|
// MARK: Swiftgram
|
|
icons = [
|
|
PresentationAppIcon(name: "SGDefault", imageName: "SGDefault", isDefault: true),
|
|
PresentationAppIcon(name: "SGBlack", imageName: "SGBlack"),
|
|
PresentationAppIcon(name: "SGLegacy", imageName: "SGLegacy"),
|
|
PresentationAppIcon(name: "SGInverted", imageName: "SGInverted"),
|
|
PresentationAppIcon(name: "SGWhite", imageName: "SGWhite"),
|
|
PresentationAppIcon(name: "SGNight", imageName: "SGNight"),
|
|
PresentationAppIcon(name: "SGSky", imageName: "SGSky"),
|
|
PresentationAppIcon(name: "SGTitanium", imageName: "SGTitanium"),
|
|
PresentationAppIcon(name: "SGNeon", imageName: "SGNeon"),
|
|
PresentationAppIcon(name: "SGNeonBlue", imageName: "SGNeonBlue"),
|
|
PresentationAppIcon(name: "SGGlass", imageName: "SGGlass"),
|
|
PresentationAppIcon(name: "SGSparkling", imageName: "SGSparkling"),
|
|
]
|
|
|
|
if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" {
|
|
icons.append(PresentationAppIcon(name: "SGBeta", imageName: "SGBeta"))
|
|
}
|
|
|
|
return icons
|
|
} else {
|
|
return []
|
|
}
|
|
}, getAlternateIconName: {
|
|
if #available(iOS 10.3, *) {
|
|
return application.alternateIconName
|
|
} else {
|
|
return nil
|
|
}
|
|
}, requestSetAlternateIconName: { name, completion in
|
|
if #available(iOS 10.3, *) {
|
|
application.setAlternateIconName(name, completionHandler: { error in
|
|
if let error = error {
|
|
Logger.shared.log("App \(self.episodeId)", "failed to set alternate icon with error \(error.localizedDescription)")
|
|
}
|
|
completion(error == nil)
|
|
})
|
|
} else {
|
|
completion(false)
|
|
}
|
|
}, forceOrientation: { orientation in
|
|
let value = orientation.rawValue
|
|
if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
|
|
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
|
var interfaceOrientations: UIInterfaceOrientationMask = []
|
|
switch orientation {
|
|
case .portrait:
|
|
interfaceOrientations = .portrait
|
|
case .landscapeLeft:
|
|
interfaceOrientations = .landscapeLeft
|
|
case .landscapeRight:
|
|
interfaceOrientations = .landscapeRight
|
|
case .portraitUpsideDown:
|
|
interfaceOrientations = .portraitUpsideDown
|
|
case .unknown:
|
|
interfaceOrientations = .portrait
|
|
@unknown default:
|
|
interfaceOrientations = .portrait
|
|
}
|
|
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: interfaceOrientations))
|
|
} else {
|
|
UIDevice.current.setValue(value, forKey: "orientation")
|
|
UINavigationController.attemptRotationToDeviceOrientation()
|
|
}
|
|
})
|
|
|
|
let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true)
|
|
self.accountManager = accountManager
|
|
|
|
telegramUIDeclareEncodables()
|
|
initializeAccountManagement()
|
|
|
|
let pushRegistry = PKPushRegistry(queue: .main)
|
|
if #available(iOS 9.0, *) {
|
|
pushRegistry.desiredPushTypes = Set([.voIP])
|
|
}
|
|
self.pushRegistry = pushRegistry
|
|
pushRegistry.delegate = self
|
|
|
|
self.accountManagerState = extractAccountManagerState(records: accountManager._internalAccountRecordsSync())
|
|
let _ = (accountManager.accountRecords()
|
|
|> deliverOnMainQueue).start(next: { view in
|
|
self.accountManagerState = extractAccountManagerState(records: view)
|
|
})
|
|
|
|
var systemUserInterfaceStyle: WindowUserInterfaceStyle = .light
|
|
if #available(iOS 13.0, *) {
|
|
if let traitCollection = window.rootViewController?.traitCollection {
|
|
systemUserInterfaceStyle = WindowUserInterfaceStyle(style: traitCollection.userInterfaceStyle)
|
|
}
|
|
}
|
|
|
|
let sharedContextSignal = currentPresentationDataAndSettings(accountManager: accountManager, systemUserInterfaceStyle: systemUserInterfaceStyle)
|
|
|> map { initialPresentationDataAndSettings -> (AccountManager, InitialPresentationDataAndSettings) in
|
|
return (accountManager, initialPresentationDataAndSettings)
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { accountManager, initialPresentationDataAndSettings -> Signal<(SharedApplicationContext, LoggingSettings), NoError> in
|
|
self.mainWindow?.hostView.containerView.backgroundColor = initialPresentationDataAndSettings.presentationData.theme.chatList.backgroundColor
|
|
|
|
let legacyBasePath = appGroupUrl.path
|
|
|
|
let presentationDataPromise = Promise<PresentationData>()
|
|
let appLockContext = AppLockContextImpl(rootPath: rootPath, window: self.mainWindow!, rootController: self.window?.rootViewController, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: {
|
|
return (self.mainWindow?.viewController as? TelegramRootController)?.chatListController?.lockViewFrame
|
|
})
|
|
|
|
var setPresentationCall: ((PresentationCall?) -> Void)?
|
|
let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in
|
|
setPresentationCall?(call)
|
|
}, navigateToChat: { accountId, peerId, messageId in
|
|
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil)
|
|
}, displayUpgradeProgress: { progress in
|
|
if let progress = progress {
|
|
if self.dataImportSplash == nil {
|
|
self.dataImportSplash = makeLegacyDataImportSplash(theme: initialPresentationDataAndSettings.presentationData.theme, strings: initialPresentationDataAndSettings.presentationData.strings)
|
|
self.dataImportSplash?.serviceAction = {
|
|
self.debugPressed()
|
|
}
|
|
self.mainWindow.coveringView = self.dataImportSplash
|
|
}
|
|
self.dataImportSplash?.progress = (.generic, progress)
|
|
} else if let dataImportSplash = self.dataImportSplash {
|
|
self.dataImportSplash = nil
|
|
if self.mainWindow.coveringView === dataImportSplash {
|
|
self.mainWindow.coveringView = nil
|
|
}
|
|
}
|
|
}, appDelegate: self)
|
|
|
|
presentationDataPromise.set(sharedContext.presentationData)
|
|
|
|
sharedContext.presentGlobalController = { [weak self] c, a in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.mainWindow.present(c, on: .root)
|
|
}
|
|
sharedContext.presentCrossfadeController = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
var exists = false
|
|
strongSelf.mainWindow.forEachViewController({ controller in
|
|
if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController || controller is ThemePreviewController {
|
|
exists = true
|
|
}
|
|
return true
|
|
})
|
|
|
|
if !exists {
|
|
strongSelf.mainWindow.present(ThemeSettingsCrossfadeController(), on: .root)
|
|
}
|
|
}
|
|
|
|
let notificationManager = SharedNotificationManager(episodeId: self.episodeId, application: application, clearNotificationsManager: clearNotificationsManager, inForeground: applicationBindings.applicationInForeground, accounts: sharedContext.activeAccountContexts |> map { primary, accounts, _ in accounts.map({ ($0.1.account, $0.1.account.id == primary?.account.id) }) }, pollLiveLocationOnce: { accountId in
|
|
let _ = (self.context.get()
|
|
|> filter {
|
|
return $0 != nil
|
|
}
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
if let context = context, context.context.account.id == accountId {
|
|
context.context.liveLocationManager?.pollOnce()
|
|
}
|
|
})
|
|
})
|
|
setPresentationCall = { call in
|
|
notificationManager.setNotificationCall(call, strings: sharedContext.currentPresentationData.with({ $0 }).strings)
|
|
}
|
|
let liveLocationPolling = self.context.get()
|
|
|> mapToSignal { context -> Signal<AccountRecordId?, NoError> in
|
|
if let context = context, let liveLocationManager = context.context.liveLocationManager {
|
|
let accountId = context.context.account.id
|
|
return combineLatest(queue: .mainQueue(),
|
|
liveLocationManager.isPolling,
|
|
liveLocationManager.hasBackgroundTasks
|
|
)
|
|
|> map { isPolling, hasBackgroundTasks -> Bool in
|
|
return isPolling || hasBackgroundTasks
|
|
}
|
|
|> distinctUntilChanged
|
|
|> map { value -> AccountRecordId? in
|
|
if value {
|
|
return accountId
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
/*let watchTasks = self.context.get()
|
|
|> mapToSignal { context -> Signal<AccountRecordId?, NoError> in
|
|
if let context = context, let watchManager = context.context.watchManager {
|
|
let accountId = context.context.account.id
|
|
let runningTasks: Signal<WatchRunningTasks?, NoError> = .single(nil)
|
|
|> then(watchManager.runningTasks)
|
|
return runningTasks
|
|
|> distinctUntilChanged
|
|
|> map { value -> AccountRecordId? in
|
|
if let value = value, value.running {
|
|
return accountId
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}*/
|
|
let wakeupManager = SharedWakeupManager(beginBackgroundTask: { name, expiration in
|
|
let id = application.beginBackgroundTask(withName: name, expirationHandler: expiration)
|
|
Logger.shared.log("App \(self.episodeId)", "Begin background task \(name): \(id)")
|
|
print("App \(self.episodeId)", "Begin background task \(name): \(id)")
|
|
return id
|
|
}, endBackgroundTask: { id in
|
|
print("App \(self.episodeId)", "End background task \(id)")
|
|
Logger.shared.log("App \(self.episodeId)", "End background task \(id)")
|
|
application.endBackgroundTask(id)
|
|
}, backgroundTimeRemaining: { application.backgroundTimeRemaining }, acquireIdleExtension: {
|
|
return applicationBindings.pushIdleTimerExtension()
|
|
}, activeAccounts: sharedContext.activeAccountContexts |> map { ($0.0?.account, $0.1.map { ($0.0, $0.1.account) }) }, liveLocationPolling: liveLocationPolling, watchTasks: .single(nil), inForeground: applicationBindings.applicationInForeground, hasActiveAudioSession: self.hasActiveAudioSession.get(), notificationManager: notificationManager, mediaManager: sharedContext.mediaManager, callManager: sharedContext.callManager, accountUserInterfaceInUse: { id in
|
|
return sharedContext.accountUserInterfaceInUse(id)
|
|
})
|
|
let sharedApplicationContext = SharedApplicationContext(sharedContext: sharedContext, notificationManager: notificationManager, wakeupManager: wakeupManager)
|
|
sharedApplicationContext.sharedContext.mediaManager.overlayMediaManager.attachOverlayMediaController(sharedApplicationContext.overlayMediaController)
|
|
|
|
return accountManager.transaction { transaction -> (SharedApplicationContext, LoggingSettings) in
|
|
return (sharedApplicationContext, transaction.getSharedData(SharedDataKeys.loggingSettings)?.get(LoggingSettings.self) ?? LoggingSettings.defaultSettings)
|
|
}
|
|
}
|
|
self.sharedContextPromise.set(sharedContextSignal
|
|
|> mapToSignal { sharedApplicationContext, loggingSettings -> Signal<SharedApplicationContext, NoError> in
|
|
Logger.shared.logToFile = loggingSettings.logToFile
|
|
Logger.shared.logToConsole = loggingSettings.logToConsole
|
|
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
|
|
|
|
return .single(sharedApplicationContext)
|
|
})
|
|
|
|
//let watchManagerArgumentsPromise = Promise<WatchManagerArguments?>()
|
|
|
|
self.context.set(self.sharedContextPromise.get()
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { sharedApplicationContext -> Signal<AuthorizedApplicationContext?, NoError> in
|
|
return sharedApplicationContext.sharedContext.activeAccountContexts
|
|
|> map { primary, _, _ -> AccountContext? in
|
|
return primary
|
|
}
|
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
|
if lhs !== rhs {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|> mapToSignal { context -> Signal<(AccountContext, CallListSettings)?, NoError> in
|
|
return sharedApplicationContext.sharedContext.accountManager.transaction { transaction -> CallListSettings? in
|
|
return transaction.getSharedData(ApplicationSpecificSharedDataKeys.callListSettings)?.get(CallListSettings.self)
|
|
}
|
|
|> reduceLeft(value: nil) { current, updated -> CallListSettings? in
|
|
var result: CallListSettings?
|
|
if let updated = updated {
|
|
result = updated
|
|
} else if let current = current {
|
|
result = current
|
|
}
|
|
return result
|
|
}
|
|
|> map { callListSettings -> (AccountContext, CallListSettings)? in
|
|
if let context = context {
|
|
return (context, callListSettings ?? .defaultSettings)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> map { accountAndSettings -> AuthorizedApplicationContext? in
|
|
return accountAndSettings.flatMap { context, callListSettings in
|
|
return AuthorizedApplicationContext(sharedApplicationContext: sharedApplicationContext, mainWindow: self.mainWindow, watchManagerArguments: .single(nil), context: context as! AccountContextImpl, accountManager: sharedApplicationContext.sharedContext.accountManager, showContactsTab: callListSettings.showContactsTab, showCallsTab: callListSettings.showTab, reinitializedNotificationSettings: {
|
|
let _ = (self.context.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
if let context = context {
|
|
self.registerForNotifications(context: context.context, authorize: false)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
self.authContext.set(self.sharedContextPromise.get()
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { sharedApplicationContext -> Signal<UnauthorizedApplicationContext?, NoError> in
|
|
return sharedApplicationContext.sharedContext.activeAccountContexts
|
|
|> map { primary, accounts, auth -> (AccountContext?, UnauthorizedAccount, [AccountContext])? in
|
|
if let auth = auth {
|
|
return (primary, auth, Array(accounts.map({ $0.1 })))
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
|
if lhs?.1 !== rhs?.1 {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|> mapToSignal { authAndAccounts -> Signal<(UnauthorizedAccount, ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]))?, NoError> in
|
|
if let (primary, auth, accounts) = authAndAccounts {
|
|
let phoneNumbers = combineLatest(accounts.map { context -> Signal<(AccountRecordId, String, Bool)?, NoError> in
|
|
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
|
|> map { peer -> (AccountRecordId, String, Bool)? in
|
|
if case let .user(user) = peer, let phone = user.phone {
|
|
return (context.account.id, phone, context.account.testingEnvironment)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
})
|
|
return phoneNumbers
|
|
|> map { phoneNumbers -> (UnauthorizedAccount, ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]))? in
|
|
var primaryNumber: (String, AccountRecordId, Bool)?
|
|
if let primary = primary {
|
|
for idAndNumber in phoneNumbers {
|
|
if let (id, number, testingEnvironment) = idAndNumber, id == primary.account.id {
|
|
primaryNumber = (number, id, testingEnvironment)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return (auth, (primaryNumber, phoneNumbers.compactMap({ $0.flatMap({ ($0.1, $0.0, $0.2) }) })))
|
|
}
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> map { accountAndSettings -> UnauthorizedApplicationContext? in
|
|
return accountAndSettings.flatMap { account, otherAccountPhoneNumbers in
|
|
return UnauthorizedApplicationContext(apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, sharedContext: sharedApplicationContext.sharedContext, account: account, otherAccountPhoneNumbers: otherAccountPhoneNumbers)
|
|
}
|
|
}
|
|
})
|
|
|
|
let contextReadyDisposable = MetaDisposable()
|
|
|
|
let startTime = CFAbsoluteTimeGetCurrent()
|
|
self.contextDisposable.set((self.context.get()
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
print("Application: context took \(CFAbsoluteTimeGetCurrent() - startTime) to become available")
|
|
|
|
var network: Network?
|
|
if let context = context {
|
|
network = context.context.account.network
|
|
// MARK: Swiftgram
|
|
sgDBResetIfNeeded(databasePath: context.context.sharedContext.accountManager.basePath + "/db", present: self.mainWindow?.presentNative)
|
|
}
|
|
|
|
Logger.shared.log("App \(self.episodeId)", "received context \(String(describing: context)) account \(String(describing: context?.context.account.id)) network \(String(describing: network))")
|
|
|
|
let firstTime = self.contextValue == nil
|
|
if let contextValue = self.contextValue {
|
|
contextValue.passcodeController?.dismiss()
|
|
contextValue.context.account.shouldExplicitelyKeepWorkerConnections.set(.single(false))
|
|
contextValue.context.account.shouldKeepBackgroundDownloadConnections.set(.single(false))
|
|
}
|
|
self.contextValue = context
|
|
if let context = context {
|
|
setupLegacyComponents(context: context.context)
|
|
let isReady = context.isReady.get()
|
|
contextReadyDisposable.set((isReady
|
|
|> filter { $0 }
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { _ in
|
|
let readyTime = CFAbsoluteTimeGetCurrent() - startTime
|
|
if readyTime > 0.5 {
|
|
print("Application: context took \(readyTime) to become ready")
|
|
}
|
|
print("Launch to ready took \((CFAbsoluteTimeGetCurrent() - launchStartTime) * 1000.0) ms")
|
|
|
|
self.mainWindow.debugAction = nil
|
|
self.mainWindow.viewController = context.rootController
|
|
|
|
if firstTime {
|
|
let layer = context.rootController.view.layer
|
|
layer.allowsGroupOpacity = true
|
|
layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak layer] _ in
|
|
if let layer = layer {
|
|
layer.allowsGroupOpacity = false
|
|
}
|
|
})
|
|
}
|
|
self.mainWindow.forEachViewController({ controller in
|
|
if let controller = controller as? TabBarAccountSwitchController {
|
|
if let rootController = self.mainWindow.viewController as? TelegramRootController {
|
|
if let tabsController = rootController.viewControllers.first as? TabBarController {
|
|
for i in 0 ..< tabsController.controllers.count {
|
|
if let _ = tabsController.controllers[i] as? (SettingsController & ViewController) {
|
|
let sourceNodes = tabsController.sourceNodesForController(at: i)
|
|
if let sourceNodes = sourceNodes {
|
|
controller.dismiss(sourceNodes: sourceNodes)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
self.mainWindow.topLevelOverlayControllers = [context.sharedApplicationContext.overlayMediaController, context.notificationController]
|
|
(context.context.sharedContext as? SharedAccountContextImpl)?.notificationController = context.notificationController
|
|
var authorizeNotifications = true
|
|
if #available(iOS 10.0, *) {
|
|
authorizeNotifications = false
|
|
}
|
|
self.registerForNotifications(context: context.context, authorize: authorizeNotifications)
|
|
|
|
self.resetIntentsIfNeeded(context: context.context)
|
|
|
|
// MARK: Swiftgram
|
|
updateSGWebSettingsInteractivelly(context: context.context)
|
|
let _ = (context.context.sharedContext.presentationData.start(next: { presentationData in
|
|
SGLocalizationManager.shared.downloadLocale(presentationData.strings.baseLanguageCode)
|
|
}))
|
|
}))
|
|
} else {
|
|
self.mainWindow.viewController = nil
|
|
self.mainWindow.topLevelOverlayControllers = []
|
|
contextReadyDisposable.set(nil)
|
|
}
|
|
}))
|
|
|
|
let authContextReadyDisposable = MetaDisposable()
|
|
|
|
self.authContextDisposable.set((self.authContext.get()
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
var network: Network?
|
|
if let context = context {
|
|
network = context.account.network
|
|
}
|
|
|
|
Logger.shared.log("App \(self.episodeId)", "received auth context \(String(describing: context)) account \(String(describing: context?.account.id)) network \(String(describing: network))")
|
|
|
|
if let authContextValue = self.authContextValue {
|
|
authContextValue.account.shouldBeServiceTaskMaster.set(.single(.never))
|
|
if authContextValue.authorizationCompleted {
|
|
let accountId = authContextValue.account.id
|
|
let _ = (self.context.get()
|
|
|> filter { context in
|
|
return context?.context.account.id == accountId
|
|
}
|
|
|> take(1)
|
|
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|
|
|> deliverOnMainQueue).start(completed: {
|
|
Queue.mainQueue().after(0.75) {
|
|
authContextValue.rootController.view.endEditing(true)
|
|
authContextValue.rootController.dismiss()
|
|
}
|
|
})
|
|
} else {
|
|
authContextValue.rootController.view.endEditing(true)
|
|
authContextValue.rootController.dismiss()
|
|
}
|
|
}
|
|
self.authContextValue = context
|
|
if let context = context {
|
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
|
|
|
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
|
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
|
self?.mainWindow.present(statusController, on: .root)
|
|
return ActionDisposable { [weak statusController] in
|
|
Queue.mainQueue().async() {
|
|
statusController?.dismiss()
|
|
}
|
|
}
|
|
}
|
|
|> runOn(Queue.mainQueue())
|
|
|> delay(0.5, queue: Queue.mainQueue())
|
|
let progressDisposable = progressSignal.start()
|
|
|
|
let isReady: Signal<Bool, NoError> = context.isReady.get()
|
|
authContextReadyDisposable.set((isReady
|
|
|> filter { $0 }
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { _ in
|
|
progressDisposable.dispose()
|
|
self.mainWindow.present(context.rootController, on: .root)
|
|
}))
|
|
} else {
|
|
authContextReadyDisposable.set(nil)
|
|
}
|
|
}))
|
|
|
|
|
|
let logoutDataSignal: Signal<(AccountManager, Set<PeerId>), NoError> = self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> mapToSignal { sharedContext -> Signal<(AccountManager<TelegramAccountManagerTypes>, Set<PeerId>), NoError> in
|
|
return sharedContext.sharedContext.activeAccountContexts
|
|
|> map { _, accounts, _ -> Set<PeerId> in
|
|
return Set(accounts.map { $0.1.account.peerId })
|
|
}
|
|
|> reduceLeft(value: Set<PeerId>()) { current, updated, emit in
|
|
if !current.isEmpty {
|
|
emit(current.subtracting(current.intersection(updated)))
|
|
}
|
|
return updated
|
|
}
|
|
|> map { loggedOutAccountPeerIds -> (AccountManager<TelegramAccountManagerTypes>, Set<PeerId>) in
|
|
return (sharedContext.sharedContext.accountManager, loggedOutAccountPeerIds)
|
|
}
|
|
}
|
|
|
|
self.logoutDisposable.set(logoutDataSignal.start(next: { accountManager, loggedOutAccountPeerIds in
|
|
let _ = (updateIntentsSettingsInteractively(accountManager: accountManager) { current in
|
|
var updated = current
|
|
for peerId in loggedOutAccountPeerIds {
|
|
deleteAllStoryDrafts(peerId: peerId)
|
|
if peerId == updated.account {
|
|
deleteAllSendMessageIntents()
|
|
updated = updated.withUpdatedAccount(nil)
|
|
break
|
|
}
|
|
}
|
|
return updated
|
|
}).start()
|
|
}))
|
|
|
|
/*self.watchCommunicationManagerPromise.set(watchCommunicationManager(context: self.context.get() |> flatMap { WatchCommunicationManagerContext(context: $0.context) }, allowBackgroundTimeExtension: { timeout in
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)).start(next: { sharedContext in
|
|
sharedContext.wakeupManager.allowBackgroundTimeExtension(timeout: timeout)
|
|
})
|
|
}))
|
|
let _ = self.watchCommunicationManagerPromise.get().start(next: { manager in
|
|
if let manager = manager {
|
|
watchManagerArgumentsPromise.set(.single(manager.arguments))
|
|
} else {
|
|
watchManagerArgumentsPromise.set(.single(nil))
|
|
}
|
|
})*/
|
|
|
|
self.resetBadge()
|
|
|
|
if #available(iOS 9.1, *) {
|
|
self.quickActionsDisposable.set((self.context.get()
|
|
|> mapToSignal { context -> Signal<[ApplicationShortcutItem], NoError> in
|
|
if let context = context {
|
|
let presentationData = context.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
return activeAccountsAndPeers(context: context.context)
|
|
|> take(1)
|
|
|> map { primaryAndAccounts -> (AccountContext, EnginePeer, Int32)? in
|
|
return primaryAndAccounts.1.first
|
|
}
|
|
|> map { accountAndPeer -> String? in
|
|
if let (_, peer, _) = accountAndPeer {
|
|
return peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
} else {
|
|
return nil
|
|
}
|
|
} |> mapToSignal { otherAccountName -> Signal<[ApplicationShortcutItem], NoError> in
|
|
let presentationData = context.context.sharedContext.currentPresentationData.with { $0 }
|
|
return .single(applicationShortcutItems(strings: presentationData.strings, otherAccountName: otherAccountName))
|
|
}
|
|
} else {
|
|
return .single([])
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).start(next: { items in
|
|
if items.isEmpty {
|
|
UIApplication.shared.shortcutItems = nil
|
|
} else {
|
|
UIApplication.shared.shortcutItems = items.map({ $0.shortcutItem() })
|
|
}
|
|
}))
|
|
}
|
|
|
|
let _ = self.isInForegroundPromise.get().start(next: { value in
|
|
Logger.shared.log("App \(self.episodeId)", "isInForeground = \(value)")
|
|
})
|
|
let _ = self.isActivePromise.get().start(next: { value in
|
|
Logger.shared.log("App \(self.episodeId)", "isActive = \(value)")
|
|
})
|
|
|
|
if let url = launchOptions?[.url] {
|
|
if let url = url as? URL, url.scheme == "tg" || url.scheme == buildConfig.appSpecificUrlScheme {
|
|
self.openUrlWhenReady(url: sgActionRequestHandlerSanitizer(url))
|
|
} else if let urlString = url as? String, urlString.lowercased().hasPrefix("tg:") || urlString.lowercased().hasPrefix("\(buildConfig.appSpecificUrlScheme):"), let url = URL(string: urlString) {
|
|
self.openUrlWhenReady(url: url)
|
|
}
|
|
}
|
|
|
|
if application.applicationState == .active {
|
|
self.isInForegroundValue = true
|
|
self.isInForegroundPromise.set(true)
|
|
self.isActiveValue = true
|
|
self.isActivePromise.set(true)
|
|
|
|
SharedDisplayLinkDriver.shared.updateForegroundState(self.isActiveValue)
|
|
|
|
self.runForegroundTasks()
|
|
}
|
|
|
|
|
|
DeviceProximityManager.shared().proximityChanged = { [weak self] value in
|
|
if let strongSelf = self {
|
|
strongSelf.mainWindow.setProximityDimHidden(!value)
|
|
}
|
|
}
|
|
|
|
if UIApplication.shared.isStatusBarHidden {
|
|
UIApplication.shared.internalSetStatusBarHidden(false, animation: .none)
|
|
}
|
|
|
|
/*if #available(iOS 13.0, *) {
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: baseAppBundleId + ".refresh", using: nil, launchHandler: { task in
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
|
|
|
|
sharedApplicationContext.wakeupManager.replaceCurrentExtensionWithExternalTime(completion: {
|
|
task.setTaskCompleted(success: true)
|
|
}, timeout: 29.0)
|
|
let _ = (self.context.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
guard let context = context else {
|
|
return
|
|
}
|
|
sharedApplicationContext.notificationManager.beginPollingState(account: context.context.account)
|
|
})
|
|
})
|
|
})
|
|
}*/
|
|
|
|
self.maybeCheckForUpdates()
|
|
|
|
#if canImport(AppCenter)
|
|
if !buildConfig.isAppStoreBuild, let appCenterId = buildConfig.appCenterId, !appCenterId.isEmpty {
|
|
AppCenter.start(withAppSecret: buildConfig.appCenterId, services: [
|
|
Crashes.self
|
|
])
|
|
}
|
|
#endif
|
|
|
|
if #available(iOS 13.0, *) {
|
|
let taskId = "\(baseAppBundleId).cleanup"
|
|
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskId, using: DispatchQueue.main) { task in
|
|
Logger.shared.log("App \(self.episodeId)", "Executing cleanup task")
|
|
|
|
let disposable = self.runCacheReindexTasks(lowImpact: true, completion: {
|
|
Logger.shared.log("App \(self.episodeId)", "Completed cleanup task")
|
|
|
|
task.setTaskCompleted(success: true)
|
|
})
|
|
|
|
task.expirationHandler = {
|
|
disposable.dispose()
|
|
task.setTaskCompleted(success: false)
|
|
}
|
|
}
|
|
|
|
BGTaskScheduler.shared.getPendingTaskRequests(completionHandler: { tasks in
|
|
if tasks.contains(where: { $0.identifier == taskId }) {
|
|
Logger.shared.log("App \(self.episodeId)", "Already have a cleanup task pending")
|
|
return
|
|
}
|
|
let request = BGProcessingTaskRequest(identifier: taskId)
|
|
request.requiresExternalPower = true
|
|
request.requiresNetworkConnectivity = false
|
|
|
|
do {
|
|
try BGTaskScheduler.shared.submit(request)
|
|
} catch let e {
|
|
Logger.shared.log("App \(self.episodeId)", "Error submitting background task request: \(e)")
|
|
}
|
|
})
|
|
}
|
|
|
|
let timestamp = Int(CFAbsoluteTimeGetCurrent())
|
|
let minReindexTimestamp = timestamp - 2 * 24 * 60 * 60
|
|
if let indexTimestamp = UserDefaults.standard.object(forKey: "TelegramCacheIndexTimestamp_v2") as? NSNumber, indexTimestamp.intValue >= minReindexTimestamp {
|
|
} else {
|
|
UserDefaults.standard.set(timestamp as NSNumber, forKey: "TelegramCacheIndexTimestamp_v2")
|
|
|
|
Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground")
|
|
let _ = self.runCacheReindexTasks(lowImpact: true, completion: {
|
|
Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground — done")
|
|
})
|
|
}
|
|
|
|
if #available(iOS 12.0, *) {
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
}
|
|
|
|
let _ = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
|
|
|
|
var previousReportedMemoryConsumption = 0
|
|
let _ = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
|
|
let value = getMemoryConsumption()
|
|
if abs(value - previousReportedMemoryConsumption) > 1 * 1024 * 1024 {
|
|
previousReportedMemoryConsumption = value
|
|
Logger.shared.log("App \(self.episodeId)", "Memory consumption: \(value / (1024 * 1024)) MB")
|
|
|
|
if self.contextValue?.context.sharedContext.immediateExperimentalUISettings.crashOnMemoryPressure == true {
|
|
let memoryUsageOverlayView: UILabel
|
|
if let current = self.memoryUsageOverlayView {
|
|
memoryUsageOverlayView = current
|
|
} else {
|
|
memoryUsageOverlayView = UILabel()
|
|
if #available(iOS 13.0, *) {
|
|
memoryUsageOverlayView.textColor = .label
|
|
} else {
|
|
memoryUsageOverlayView.textColor = .black
|
|
}
|
|
memoryUsageOverlayView.font = Font.regular(11.0)
|
|
memoryUsageOverlayView.layer.zPosition = 1000.0
|
|
self.memoryUsageOverlayView = memoryUsageOverlayView
|
|
self.window?.addSubview(memoryUsageOverlayView)
|
|
|
|
memoryUsageOverlayView.center = CGPoint(x: 5.0, y: 36.0)
|
|
}
|
|
|
|
memoryUsageOverlayView.text = "\(value / (1024 * 1024)) MB"
|
|
memoryUsageOverlayView.sizeToFit()
|
|
} else {
|
|
if let memoryUsageOverlayView = self.memoryUsageOverlayView {
|
|
self.memoryUsageOverlayView = nil
|
|
memoryUsageOverlayView.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
if !buildConfig.isAppStoreBuild {
|
|
if value >= 2000 * 1024 * 1024 {
|
|
if self.contextValue?.context.sharedContext.immediateExperimentalUISettings.crashOnMemoryPressure == true {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
//self.addBackgroundDownloadTask()
|
|
|
|
let reflectorBenchmarkDisposable = MetaDisposable()
|
|
let runReflectorBenchmarkDisposable = MetaDisposable()
|
|
let _ = (self.context.get()
|
|
|> deliverOnMainQueue).startStandalone(next: { context in
|
|
reflectorBenchmarkDisposable.set(nil)
|
|
runReflectorBenchmarkDisposable.set(nil)
|
|
|
|
guard let context = context?.context else {
|
|
return
|
|
}
|
|
var defaultAutoBenchmarkReflectors = false
|
|
if case .internal = context.sharedContext.applicationBindings.appBuildType {
|
|
defaultAutoBenchmarkReflectors = true
|
|
}
|
|
if context.sharedContext.immediateExperimentalUISettings.autoBenchmarkReflectors ?? defaultAutoBenchmarkReflectors {
|
|
reflectorBenchmarkDisposable.set((context.sharedContext.applicationBindings.applicationInForeground
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).startStrict(next: { value in
|
|
if value {
|
|
let signal: Signal<ReflectorBenchmark.Results, NoError> = Signal { subscriber in
|
|
var reflectorBenchmark: ReflectorBenchmark? = ReflectorBenchmark(address: "91.108.13.35", port: 599)
|
|
reflectorBenchmark?.start(completion: { results in
|
|
subscriber.putNext(results)
|
|
subscriber.putCompletion()
|
|
})
|
|
|
|
return ActionDisposable {
|
|
reflectorBenchmark = nil
|
|
}
|
|
}
|
|
|> runOn(.mainQueue())
|
|
|> delay(Double.random(in: 1.0 ..< 5.0), queue: Queue.mainQueue())
|
|
runReflectorBenchmarkDisposable.set(signal.startStrict(next: { results in
|
|
print("Reflector banchmark:\nBandwidth: \(results.bandwidthBytesPerSecond * 8 / 1024) kbit/s (expected \(results.expectedBandwidthBytesPerSecond * 8 / 1024) kbit/s)\nAvg latency: \(Int(results.averageDelay * 1000.0)) ms")
|
|
}))
|
|
} else {
|
|
runReflectorBenchmarkDisposable.set(nil)
|
|
}
|
|
}))
|
|
}
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
private var backgroundSessionSourceDataDisposables: [String: Disposable] = [:]
|
|
private var backgroundUploadResultSubscribers: [String: Bag<(String?) -> Void>] = [:]
|
|
|
|
func uploadInBackround(postbox: Postbox, resource: MediaResource) -> Signal<String?, NoError> {
|
|
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
|
let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
|
|
|
|
let signal = Signal<Never, NoError> { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
let _ = session.getAllTasks(completionHandler: { tasks in
|
|
var alreadyExists = false
|
|
for task in tasks {
|
|
if let originalRequest = task.originalRequest {
|
|
if let requestResourceId = originalRequest.value(forHTTPHeaderField: "tresource") {
|
|
if resource.id.stringRepresentation == requestResourceId {
|
|
alreadyExists = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !alreadyExists, self.backgroundSessionSourceDataDisposables[resource.id.stringRepresentation] == nil {
|
|
self.backgroundSessionSourceDataDisposables[resource.id.stringRepresentation] = (Signal<Never, NoError> { subscriber in
|
|
let dataDisposable = (postbox.mediaBox.resourceData(resource)
|
|
|> deliverOnMainQueue).start(next: { data in
|
|
if data.complete {
|
|
self.addBackgroundUploadTask(id: resource.id.stringRepresentation, path: data.path)
|
|
}
|
|
})
|
|
let fetchDisposable = postbox.mediaBox.fetchedResource(resource, parameters: nil).start()
|
|
|
|
return ActionDisposable {
|
|
dataDisposable.dispose()
|
|
fetchDisposable.dispose()
|
|
}
|
|
}).start()
|
|
}
|
|
})
|
|
|
|
return disposable
|
|
}
|
|
|> runOn(.mainQueue())
|
|
|
|
return Signal { subscriber in
|
|
let bag: Bag<(String?) -> Void>
|
|
if let current = self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] {
|
|
bag = current
|
|
} else {
|
|
bag = Bag()
|
|
self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] = bag
|
|
}
|
|
let index = bag.add { result in
|
|
subscriber.putNext(result)
|
|
subscriber.putCompletion()
|
|
}
|
|
|
|
let workDisposable = signal.start()
|
|
|
|
return ActionDisposable {
|
|
workDisposable.dispose()
|
|
|
|
Queue.mainQueue().async {
|
|
if let bag = self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] {
|
|
bag.remove(index)
|
|
if bag.isEmpty {
|
|
//TODO:cancel tasks
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|> runOn(.mainQueue())
|
|
}
|
|
|
|
private func addBackgroundUploadTask(id: String, path: String) {
|
|
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
|
let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
|
|
|
|
let fileName = "upload-\(UInt32.random(in: 0 ... UInt32.max))"
|
|
let uploadFilePath = NSTemporaryDirectory() + "/" + fileName
|
|
guard let sourceFile = ManagedFile(queue: nil, path: uploadFilePath, mode: .readwrite) else {
|
|
return
|
|
}
|
|
guard let inFile = ManagedFile(queue: nil, path: path, mode: .read) else {
|
|
return
|
|
}
|
|
|
|
let boundary = UUID().uuidString
|
|
|
|
var headerData = Data()
|
|
headerData.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
|
|
headerData.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
|
|
headerData.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
|
|
|
|
var footerData = Data()
|
|
footerData.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
|
|
|
let _ = sourceFile.write(headerData)
|
|
|
|
let bufferSize = 512 * 1024
|
|
let buffer = malloc(bufferSize)!
|
|
defer {
|
|
free(buffer)
|
|
}
|
|
|
|
while true {
|
|
let readBytes = inFile.read(buffer, bufferSize)
|
|
if readBytes <= 0 {
|
|
break
|
|
} else {
|
|
let _ = sourceFile.write(buffer, count: readBytes)
|
|
}
|
|
}
|
|
|
|
let _ = sourceFile.write(footerData)
|
|
|
|
sourceFile._unsafeClose()
|
|
inFile._unsafeClose()
|
|
|
|
var request = URLRequest(url: URL(string: "http://localhost:25478/upload?token=f9403fc5f537b4ab332d")!)
|
|
request.httpMethod = "POST"
|
|
|
|
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
|
request.setValue(id, forHTTPHeaderField: "tresource")
|
|
|
|
let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: uploadFilePath))
|
|
task.resume()
|
|
}
|
|
|
|
private func addBackgroundDownloadTask() {
|
|
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
|
let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
|
|
|
|
var request = URLRequest(url: URL(string: "https://example.com/\(UInt64.random(in: 0 ... UInt64.max))")!)
|
|
request.httpMethod = "GET"
|
|
|
|
let task = session.downloadTask(with: request)
|
|
Logger.shared.log("App \(self.episodeId)", "adding download task \(String(describing: request.url))")
|
|
task.earliestBeginDate = Date(timeIntervalSinceNow: 30.0)
|
|
task.resume()
|
|
}
|
|
|
|
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
Logger.shared.log("App \(self.episodeId)", "completed download task \(String(describing: task.originalRequest?.url)) error: \(String(describing: error))")
|
|
if let response = task.response as? HTTPURLResponse {
|
|
if let originalRequest = task.originalRequest {
|
|
if let requestResourceId = originalRequest.value(forHTTPHeaderField: "tresource") {
|
|
if let bag = self.backgroundUploadResultSubscribers[requestResourceId] {
|
|
for item in bag.copyItems() {
|
|
item("http server: \(response.allHeaderFields)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func runCacheReindexTasks(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable {
|
|
let disposable = MetaDisposable()
|
|
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
|
|
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { activeAccounts in
|
|
var signals: Signal<Never, NoError> = .complete()
|
|
|
|
for (_, context, _) in activeAccounts.accounts {
|
|
signals = signals |> then(context.account.cleanupTasks(lowImpact: lowImpact))
|
|
}
|
|
|
|
disposable.set(signals.start(completed: {
|
|
completion()
|
|
}))
|
|
})
|
|
})
|
|
|
|
return disposable
|
|
}
|
|
|
|
private func resetBadge() {
|
|
var resetOnce = true
|
|
self.badgeDisposable.set((self.context.get()
|
|
|> mapToSignal { context -> Signal<Int32, NoError> in
|
|
if let context = context {
|
|
return context.applicationBadge
|
|
} else {
|
|
return .single(0)
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).start(next: { count in
|
|
if resetOnce {
|
|
resetOnce = false
|
|
if count == 0 {
|
|
//UIApplication.shared.applicationIconBadgeNumber = 1
|
|
}
|
|
}
|
|
UIApplication.shared.applicationIconBadgeNumber = Int(count)
|
|
}))
|
|
}
|
|
|
|
func applicationWillResignActive(_ application: UIApplication) {
|
|
self.isActiveValue = false
|
|
self.isActivePromise.set(false)
|
|
self.clearNotificationsManager?.commitNow()
|
|
|
|
if let navigationController = self.mainWindow.viewController as? NavigationController {
|
|
for controller in navigationController.viewControllers {
|
|
if let controller = controller as? TabBarController {
|
|
for subController in controller.controllers {
|
|
subController.forEachController { controller in
|
|
if let controller = controller as? UndoOverlayController {
|
|
controller.dismissWithCommitAction()
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.mainWindow.forEachViewController({ controller in
|
|
if let controller = controller as? UndoOverlayController {
|
|
controller.dismissWithCommitAction()
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
|
|
var extendNow = false
|
|
if #available(iOS 9.0, *) {
|
|
if !ProcessInfo.processInfo.isLowPowerModeEnabled {
|
|
extendNow = true
|
|
}
|
|
}
|
|
if !sharedApplicationContext.sharedContext.energyUsageSettings.extendBackgroundWork {
|
|
extendNow = false
|
|
}
|
|
sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0, extendNow: extendNow)
|
|
})
|
|
|
|
self.isInForegroundValue = false
|
|
self.isInForegroundPromise.set(false)
|
|
self.isActiveValue = false
|
|
self.isActivePromise.set(false)
|
|
|
|
final class TaskIdHolder {
|
|
var taskId: UIBackgroundTaskIdentifier?
|
|
}
|
|
|
|
let taskIdHolder = TaskIdHolder()
|
|
|
|
taskIdHolder.taskId = application.beginBackgroundTask(withName: "lock", expirationHandler: {
|
|
if let taskId = taskIdHolder.taskId {
|
|
UIApplication.shared.endBackgroundTask(taskId)
|
|
}
|
|
})
|
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: {
|
|
if let taskId = taskIdHolder.taskId {
|
|
UIApplication.shared.endBackgroundTask(taskId)
|
|
}
|
|
})
|
|
}
|
|
|
|
func applicationWillEnterForeground(_ application: UIApplication) {
|
|
if self.isActiveValue {
|
|
self.isInForegroundValue = true
|
|
self.isInForegroundPromise.set(true)
|
|
} else {
|
|
if #available(iOSApplicationExtension 12.0, *) {
|
|
DispatchQueue.main.async {
|
|
self.isInForegroundValue = true
|
|
self.isInForegroundPromise.set(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.runForegroundTasks()
|
|
|
|
SharedDisplayLinkDriver.shared.updateForegroundState(self.isActiveValue)
|
|
}
|
|
|
|
func runForegroundTasks(onlySG: Bool = false) {
|
|
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
|
|
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { activeAccounts in
|
|
for (_, context, _) in activeAccounts.accounts {
|
|
// MARK: Swiftgram
|
|
updateSGWebSettingsInteractivelly(context: context)
|
|
if onlySG {
|
|
continue
|
|
}
|
|
(context.downloadedMediaStoreManager as? DownloadedMediaStoreManagerImpl)?.runTasks()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
|
self.isInForegroundValue = true
|
|
self.isInForegroundPromise.set(true)
|
|
self.isActiveValue = true
|
|
self.isActivePromise.set(true)
|
|
|
|
self.resetBadge()
|
|
|
|
self.maybeCheckForUpdates()
|
|
|
|
SharedDisplayLinkDriver.shared.updateForegroundState(self.isActiveValue)
|
|
}
|
|
|
|
func applicationWillTerminate(_ application: UIApplication) {
|
|
Logger.shared.log("App \(self.episodeId)", "terminating")
|
|
}
|
|
|
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: didRegisterForRemoteNotificationsWithDeviceToken (deviceToken: \(hexString(deviceToken)))")
|
|
self.notificationTokenPromise.set(.single(deviceToken))
|
|
}
|
|
|
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: didFailToRegisterForRemoteNotificationsWithError (error: \(error))")
|
|
}
|
|
|
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
|
|
sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0)
|
|
})
|
|
|
|
var redactedPayload = userInfo
|
|
if var aps = redactedPayload["aps"] as? [AnyHashable: Any] {
|
|
if Logger.shared.redactSensitiveData {
|
|
if aps["alert"] != nil {
|
|
aps["alert"] = "[[redacted]]"
|
|
}
|
|
if aps["body"] != nil {
|
|
aps["body"] = "[[redacted]]"
|
|
}
|
|
}
|
|
redactedPayload["aps"] = aps
|
|
}
|
|
|
|
Logger.shared.log("App \(self.episodeId)", "remoteNotification: \(redactedPayload)")
|
|
|
|
if let firebaseAuth = redactedPayload["com.google.firebase.auth"] as? String {
|
|
guard let firebaseAuthData = firebaseAuth.data(using: .utf8), let firebaseJson = try? JSONSerialization.jsonObject(with: firebaseAuthData) else {
|
|
completionHandler(.newData)
|
|
return
|
|
}
|
|
guard let firebaseDict = firebaseJson as? [String: Any] else {
|
|
completionHandler(.newData)
|
|
return
|
|
}
|
|
|
|
if let receipt = firebaseDict["receipt"] as? String, let secret = firebaseDict["secret"] as? String {
|
|
var firebaseSecrets = self.firebaseSecrets
|
|
firebaseSecrets[receipt] = secret
|
|
self.firebaseSecrets = firebaseSecrets
|
|
}
|
|
|
|
completionHandler(.newData)
|
|
return
|
|
}
|
|
|
|
if let nonce = redactedPayload["verify_nonce"] as? String, let secret = redactedPayload["verify_secret"] as? String {
|
|
var firebaseRequestVerificationSecrets = self.firebaseRequestVerificationSecrets
|
|
firebaseRequestVerificationSecrets[nonce] = secret
|
|
self.firebaseRequestVerificationSecrets = firebaseRequestVerificationSecrets
|
|
|
|
completionHandler(.newData)
|
|
return
|
|
}
|
|
|
|
if userInfo["p"] == nil {
|
|
completionHandler(.noData)
|
|
return
|
|
}
|
|
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
|
|
sharedApplicationContext.wakeupManager.replaceCurrentExtensionWithExternalTime(completion: {
|
|
completionHandler(.newData)
|
|
}, timeout: 29.0)
|
|
sharedApplicationContext.notificationManager.addNotification(userInfo)
|
|
})
|
|
}
|
|
|
|
/*func application(_ application: UIApplication, didReceive notification: UILocalNotification) {
|
|
if (application.applicationState == .inactive) {
|
|
Logger.shared.log("App \(self.episodeId)", "tap local notification \(String(describing: notification.userInfo)), applicationState \(application.applicationState)")
|
|
}
|
|
}*/
|
|
|
|
public func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
|
|
if #available(iOS 9.0, *) {
|
|
if case PKPushType.voIP = type {
|
|
Logger.shared.log("App \(self.episodeId)", "pushRegistry credentials: \(credentials.token as NSData)")
|
|
|
|
self.voipTokenPromise.set(.single(credentials.token))
|
|
}
|
|
}
|
|
}
|
|
|
|
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry didReceiveIncomingPushWith \(payload.dictionaryPayload)")
|
|
|
|
self.pushRegistryImpl(registry, didReceiveIncomingPushWith: payload, for: type, completion: completion)
|
|
}
|
|
|
|
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry didReceiveIncomingPushWith \(payload.dictionaryPayload)")
|
|
|
|
self.pushRegistryImpl(registry, didReceiveIncomingPushWith: payload, for: type, completion: {})
|
|
}
|
|
|
|
private func pushRegistryImpl(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry processing push notification")
|
|
|
|
let decryptedPayloadAndAccountId: ([AnyHashable: Any], AccountRecordId)?
|
|
|
|
if let accountIdString = payload.dictionaryPayload["accountId"] as? String, let accountId = Int64(accountIdString) {
|
|
decryptedPayloadAndAccountId = (payload.dictionaryPayload, AccountRecordId(rawValue: accountId))
|
|
} else {
|
|
guard var encryptedPayload = payload.dictionaryPayload["p"] as? String else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "encryptedPayload is nil")
|
|
completion()
|
|
return
|
|
}
|
|
encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+")
|
|
encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/")
|
|
while encryptedPayload.count % 4 != 0 {
|
|
encryptedPayload.append("=")
|
|
}
|
|
guard let payloadData = Data(base64Encoded: encryptedPayload) else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode encryptedPayload")
|
|
completion()
|
|
return
|
|
}
|
|
guard let keyId = notificationPayloadKeyId(data: payloadData) else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't parse payload key id")
|
|
completion()
|
|
return
|
|
}
|
|
guard let accountManagerState = self.accountManagerState else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "accountManagerState is nil")
|
|
completion()
|
|
return
|
|
}
|
|
|
|
var maybeAccountId: AccountRecordId?
|
|
var maybeNotificationKey: MasterNotificationKey?
|
|
|
|
for key in accountManagerState.notificationKeys {
|
|
if key.id == keyId {
|
|
maybeAccountId = key.accountId
|
|
maybeNotificationKey = MasterNotificationKey(id: key.id, data: key.key)
|
|
break
|
|
}
|
|
}
|
|
|
|
guard let accountId = maybeAccountId, let notificationKey = maybeNotificationKey else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "accountId or notificationKey is nil")
|
|
completion()
|
|
return
|
|
}
|
|
guard let decryptedPayload = decryptedNotificationPayload(key: notificationKey, data: payloadData) else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decrypt payload")
|
|
completion()
|
|
return
|
|
}
|
|
guard let payloadJson = try? JSONSerialization.jsonObject(with: decryptedPayload, options: []) as? [AnyHashable: Any] else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode payload json")
|
|
completion()
|
|
return
|
|
}
|
|
|
|
decryptedPayloadAndAccountId = (payloadJson, accountId)
|
|
}
|
|
|
|
guard let (payloadJson, accountId) = decryptedPayloadAndAccountId else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "decryptedPayloadAndAccountId is nil")
|
|
completion()
|
|
return
|
|
}
|
|
|
|
guard var updateString = payloadJson["updates"] as? String else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "updates is nil")
|
|
completion()
|
|
return
|
|
}
|
|
|
|
updateString = updateString.replacingOccurrences(of: "-", with: "+")
|
|
updateString = updateString.replacingOccurrences(of: "_", with: "/")
|
|
while updateString.count % 4 != 0 {
|
|
updateString.append("=")
|
|
}
|
|
guard let updateData = Data(base64Encoded: updateString) else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode updateData")
|
|
completion()
|
|
return
|
|
}
|
|
guard let callUpdate = AccountStateManager.extractIncomingCallUpdate(data: updateData) else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't extract call update")
|
|
completion()
|
|
return
|
|
}
|
|
guard let callKitIntegration = CallKitIntegration.shared else {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "CallKitIntegration is not available")
|
|
completion()
|
|
return
|
|
}
|
|
|
|
let phoneNumber = payloadJson["phoneNumber"] as? String
|
|
|
|
callKitIntegration.reportIncomingCall(
|
|
uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId),
|
|
stableId: callUpdate.callId,
|
|
handle: "\(callUpdate.peer.id.id._internalGetInt64Value())",
|
|
phoneNumber: phoneNumber.flatMap(formatPhoneNumber),
|
|
isVideo: callUpdate.isVideo,
|
|
displayTitle: callUpdate.peer.debugDisplayTitle,
|
|
completion: { error in
|
|
if let error = error {
|
|
if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) {
|
|
Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode")
|
|
} else {
|
|
Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)")
|
|
/*Queue.mainQueue().async {
|
|
if let strongSelf = self {
|
|
strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp, debugLog: .single(nil))
|
|
}
|
|
}*/
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
|
|
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { activeAccounts in
|
|
var processed = false
|
|
for (_, context, _) in activeAccounts.accounts {
|
|
if context.account.id == accountId {
|
|
context.account.stateManager.processIncomingCallUpdate(data: updateData, completion: { _ in
|
|
})
|
|
|
|
//callUpdate.callId
|
|
let disposable = MetaDisposable()
|
|
self.watchedCallsDisposables.add(disposable)
|
|
|
|
disposable.set((context.account.callSessionManager.callState(internalId: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId))
|
|
|> deliverOnMainQueue).start(next: { state in
|
|
switch state.state {
|
|
case .terminated:
|
|
callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId))
|
|
default:
|
|
break
|
|
}
|
|
}))
|
|
|
|
processed = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !processed {
|
|
callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId))
|
|
}
|
|
})
|
|
|
|
sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0)
|
|
|
|
if case PKPushType.voIP = type {
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry payload: \(payload.dictionaryPayload)")
|
|
sharedApplicationContext.notificationManager.addNotification(payload.dictionaryPayload)
|
|
}
|
|
})
|
|
|
|
Logger.shared.log("App \(self.episodeId) PushRegistry", "Invoking completion handler")
|
|
|
|
completion()
|
|
}
|
|
|
|
public func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
|
Logger.shared.log("App \(self.episodeId)", "invalidated token for \(type)")
|
|
}
|
|
|
|
private func authorizedContext() -> Signal<AuthorizedApplicationContext, NoError> {
|
|
return self.context.get()
|
|
|> mapToSignal { context -> Signal<AuthorizedApplicationContext, NoError> in
|
|
if let context = context {
|
|
return .single(context)
|
|
} else {
|
|
return .complete()
|
|
}
|
|
}
|
|
}
|
|
|
|
func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool {
|
|
self.openUrl(url: url)
|
|
return true
|
|
}
|
|
|
|
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
|
|
self.openUrl(url: url)
|
|
return true
|
|
}
|
|
|
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
|
guard self.openUrlInProgress != url else {
|
|
return true
|
|
}
|
|
|
|
self.openUrl(url: url)
|
|
return true
|
|
}
|
|
|
|
func application(_ application: UIApplication, handleOpen url: URL) -> Bool {
|
|
self.openUrl(url: url)
|
|
return true
|
|
}
|
|
|
|
private func openUrl(url: URL) {
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> mapToSignal { sharedApplicationContext -> Signal<(SharedAccountContextImpl, AuthorizedApplicationContext?, UnauthorizedApplicationContext?), NoError> in
|
|
combineLatest(self.context.get(), self.authContext.get())
|
|
|> filter { $0 != nil || $1 != nil }
|
|
|> take(1)
|
|
|> map { context, authContext -> (SharedAccountContextImpl, AuthorizedApplicationContext?, UnauthorizedApplicationContext?) in
|
|
return (sharedApplicationContext.sharedContext, context, authContext)
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).start(next: { sharedContext, context, authContext in
|
|
let url = sgActionRequestHandlerSanitizer(url)
|
|
if let authContext = authContext, let confirmationCode = parseConfirmationCodeUrl(sharedContext: sharedContext, url: url) {
|
|
authContext.rootController.applyConfirmationCode(confirmationCode)
|
|
} else if let context = context {
|
|
context.openUrl(url)
|
|
} else if let authContext = authContext {
|
|
if let proxyData = parseProxyUrl(sharedContext: sharedContext, url: url) {
|
|
authContext.rootController.view.endEditing(true)
|
|
let presentationData = authContext.sharedContext.currentPresentationData.with { $0 }
|
|
let controller = ProxyServerActionSheetController(presentationData: presentationData, accountManager: authContext.sharedContext.accountManager, postbox: authContext.account.postbox, network: authContext.account.network, server: proxyData, updatedPresentationData: nil)
|
|
authContext.rootController.currentWindow?.present(controller, on: PresentationSurfaceLevel.root, blockInteraction: false, completion: {})
|
|
} else if let secureIdData = parseSecureIdUrl(url) {
|
|
let presentationData = authContext.sharedContext.currentPresentationData.with { $0 }
|
|
authContext.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Passport_NotLoggedInMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Calls_NotNow, action: {
|
|
if let callbackUrl = URL(string: secureIdCallbackUrl(with: secureIdData.callbackUrl, peerId: secureIdData.peerId, result: .cancel, parameters: [:])) {
|
|
UIApplication.shared.open(callbackUrl, options: [:], completionHandler: nil)
|
|
}
|
|
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
|
if #available(iOS 10.0, *) {
|
|
var startCallContacts: [INPerson]?
|
|
var isVideo = false
|
|
if let startCallIntent = userActivity.interaction?.intent as? SupportedStartCallIntent {
|
|
startCallContacts = startCallIntent.contacts
|
|
isVideo = false
|
|
} else if let startCallIntent = userActivity.interaction?.intent as? SupportedStartVideoCallIntent {
|
|
startCallContacts = startCallIntent.contacts
|
|
isVideo = true
|
|
}
|
|
|
|
if let startCallContacts = startCallContacts {
|
|
let startCall: (PeerId) -> Void = { peerId in
|
|
self.startCallWhenReady(accountId: nil, peerId: peerId, isVideo: isVideo)
|
|
}
|
|
|
|
func cleanPhoneNumber(_ text: String) -> String {
|
|
var result = ""
|
|
for c in text {
|
|
if c == "+" {
|
|
if result.isEmpty {
|
|
result += String(c)
|
|
}
|
|
} else if c >= "0" && c <= "9" {
|
|
result += String(c)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func matchPhoneNumbers(_ lhs: String, _ rhs: String) -> Bool {
|
|
if lhs.count < 10 && lhs.count == rhs.count {
|
|
return lhs == rhs
|
|
} else if lhs.count >= 10 && rhs.count >= 10 && lhs.suffix(10) == rhs.suffix(10) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if let contact = startCallContacts.first {
|
|
let contactByIdentifier: Signal<EnginePeer?, NoError>
|
|
if let context = self.contextValue?.context, let contactIdentifier = contact.contactIdentifier {
|
|
contactByIdentifier = context.engine.contacts.findPeerByLocalContactIdentifier(identifier: contactIdentifier)
|
|
} else {
|
|
contactByIdentifier = .single(nil)
|
|
}
|
|
|
|
let _ = (contactByIdentifier |> deliverOnMainQueue).start(next: { peerByContact in
|
|
var processed = false
|
|
if let peerByContact = peerByContact {
|
|
startCall(peerByContact.id)
|
|
processed = true
|
|
} else if let handle = contact.customIdentifier, handle.hasPrefix("tg") {
|
|
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
|
|
if let value = Int64(string) {
|
|
startCall(PeerId(value))
|
|
processed = true
|
|
}
|
|
}
|
|
if !processed, let handle = contact.personHandle, let value = handle.value {
|
|
switch handle.type {
|
|
case .unknown:
|
|
if let value = Int64(value) {
|
|
startCall(PeerId(value))
|
|
processed = true
|
|
}
|
|
case .phoneNumber:
|
|
let phoneNumber = cleanPhoneNumber(value)
|
|
if !phoneNumber.isEmpty {
|
|
guard let context = self.contextValue?.context else {
|
|
return
|
|
}
|
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
|
|
|> map { contactList -> PeerId? in
|
|
var result: PeerId?
|
|
for peer in contactList.peers {
|
|
if case let .user(peer) = peer, let peerPhoneNumber = peer.phone {
|
|
if matchPhoneNumbers(phoneNumber, peerPhoneNumber) {
|
|
result = peer.id
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|> deliverOnMainQueue).start(next: { peerId in
|
|
if let peerId = peerId {
|
|
startCall(peerId)
|
|
}
|
|
})
|
|
processed = true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
return true
|
|
}
|
|
} else if let sendMessageIntent = userActivity.interaction?.intent as? INSendMessageIntent {
|
|
if let contact = sendMessageIntent.recipients?.first, let handle = contact.customIdentifier, handle.hasPrefix("tg") {
|
|
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
|
|
if let value = Int64(string) {
|
|
self.openChatWhenReady(accountId: nil, peerId: PeerId(value), threadId: nil, activateInput: true, storyId: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {
|
|
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?, [AccountContext?]), NoError> in
|
|
return sharedApplicationContext.sharedContext.activeAccountContexts
|
|
|> take(1)
|
|
|> mapToSignal { primary, contexts, _ -> Signal<(AccountRecordId?, [AccountContext?]), NoError> in
|
|
return combineLatest(contexts.map { _, context, _ -> Signal<AccountContext?, NoError> in
|
|
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
|
|> map { peer -> AccountContext? in
|
|
if peer != nil {
|
|
return context
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
})
|
|
|> map { contexts -> (AccountRecordId?, [AccountContext?]) in
|
|
return (primary?.account.id, contexts)
|
|
}
|
|
}
|
|
}
|
|
let _ = (signal
|
|
|> deliverOnMainQueue).start(next: { primary, contexts in
|
|
if let primary = primary {
|
|
for context in contexts {
|
|
if let context = context, context.account.id == primary {
|
|
self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
for context in contexts {
|
|
if let context = context {
|
|
self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
@available(iOS 9.0, *)
|
|
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
|
let _ = (self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedContext in
|
|
let type = ApplicationShortcutItemType(rawValue: shortcutItem.type)
|
|
let immediately = type == .account
|
|
let proceed: () -> Void = {
|
|
let _ = (self.context.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
if let context = context {
|
|
if let type = type {
|
|
switch type {
|
|
case .search:
|
|
context.openRootSearch()
|
|
case .compose:
|
|
context.openRootCompose()
|
|
case .camera:
|
|
context.openRootCamera()
|
|
case .savedMessages:
|
|
self.openChatWhenReady(accountId: nil, peerId: context.context.account.peerId, threadId: nil, storyId: nil)
|
|
case .account:
|
|
context.switchAccount()
|
|
case .appIcon:
|
|
context.openAppIcon()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if let appLockContext = sharedContext.sharedContext.appLockContext as? AppLockContextImpl, !immediately {
|
|
let _ = (appLockContext.isCurrentlyLocked
|
|
|> filter { !$0 }
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { _ in
|
|
proceed()
|
|
})
|
|
} else {
|
|
proceed()
|
|
}
|
|
})
|
|
}
|
|
|
|
private func openNotificationSettingsWhenReady() {
|
|
let _ = (self.authorizedContext()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
context.openNotificationSettings()
|
|
})
|
|
}
|
|
|
|
private func startCallWhenReady(accountId: AccountRecordId?, peerId: PeerId, isVideo: Bool) {
|
|
let signal = self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> mapToSignal { sharedApplicationContext -> Signal<AuthorizedApplicationContext, NoError> in
|
|
if let accountId = accountId {
|
|
sharedApplicationContext.sharedContext.switchToAccount(id: accountId)
|
|
return self.authorizedContext()
|
|
|> filter { context in
|
|
context.context.account.id == accountId
|
|
}
|
|
|> take(1)
|
|
} else {
|
|
return self.authorizedContext()
|
|
|> take(1)
|
|
}
|
|
}
|
|
self.openChatWhenReadyDisposable.set((signal
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
context.startCall(peerId: peerId, isVideo: isVideo)
|
|
}))
|
|
}
|
|
|
|
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
|
|
let signal = self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { sharedApplicationContext -> Signal<AuthorizedApplicationContext, NoError> in
|
|
if let accountId = accountId {
|
|
sharedApplicationContext.sharedContext.switchToAccount(id: accountId)
|
|
return self.authorizedContext()
|
|
|> filter { context in
|
|
context.context.account.id == accountId
|
|
}
|
|
|> take(1)
|
|
} else {
|
|
return self.authorizedContext()
|
|
|> take(1)
|
|
}
|
|
}
|
|
self.openChatWhenReadyDisposable.set((signal
|
|
|> deliverOnMainQueue).start(next: { context in
|
|
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny)
|
|
}))
|
|
}
|
|
|
|
private var openUrlInProgress: URL?
|
|
private func openUrlWhenReady(url: URL) {
|
|
self.openUrlInProgress = url
|
|
|
|
self.openUrlWhenReadyDisposable.set((self.authorizedContext()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] context in
|
|
context.openUrl(url)
|
|
|
|
Queue.mainQueue().after(1.0, {
|
|
self?.openUrlInProgress = nil
|
|
})
|
|
}))
|
|
}
|
|
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
|
let _ = (accountIdFromNotification(response.notification, sharedContext: self.sharedContextPromise.get())
|
|
|> deliverOnMainQueue).start(next: { accountId in
|
|
if response.actionIdentifier == UNNotificationDefaultActionIdentifier {
|
|
if let (peerId, threadId) = peerIdFromNotification(response.notification) {
|
|
var messageId: MessageId? = nil
|
|
if response.notification.request.content.categoryIdentifier == "c" || response.notification.request.content.categoryIdentifier == "t" {
|
|
messageId = messageIdFromNotification(peerId: peerId, notification: response.notification)
|
|
}
|
|
let storyId = storyIdFromNotification(peerId: peerId, notification: response.notification)
|
|
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: threadId, messageId: messageId, storyId: storyId)
|
|
}
|
|
completionHandler()
|
|
} else if response.actionIdentifier == "reply", let (peerId, threadId) = peerIdFromNotification(response.notification), let accountId = accountId {
|
|
guard let response = response as? UNTextInputNotificationResponse, !response.userText.isEmpty else {
|
|
completionHandler()
|
|
return
|
|
}
|
|
let text = response.userText
|
|
let signal = self.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { sharedContext -> Signal<Void, NoError> in
|
|
sharedContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0, extendNow: true)
|
|
return sharedContext.sharedContext.activeAccountContexts
|
|
|> mapToSignal { _, contexts, _ -> Signal<Account, NoError> in
|
|
for context in contexts {
|
|
if context.1.account.id == accountId {
|
|
return .single(context.1.account)
|
|
}
|
|
}
|
|
return .complete()
|
|
}
|
|
|> take(1)
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { account -> Signal<Void, NoError> in
|
|
if let messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) {
|
|
let _ = TelegramEngine(account: account).messages.applyMaxReadIndexInteractively(index: MessageIndex(id: messageId, timestamp: 0)).start()
|
|
}
|
|
var replyToMessageId: MessageId?
|
|
if let threadId {
|
|
replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))
|
|
}
|
|
return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
|
|> map { messageIds -> MessageId? in
|
|
if messageIds.isEmpty {
|
|
return nil
|
|
} else {
|
|
return messageIds[0]
|
|
}
|
|
}
|
|
|> mapToSignal { messageId -> Signal<Void, NoError> in
|
|
if let messageId = messageId {
|
|
return account.postbox.unsentMessageIdsView()
|
|
|> filter { view in
|
|
return !view.ids.contains(messageId)
|
|
}
|
|
|> take(1)
|
|
|> mapToSignal { _ -> Signal<Void, NoError> in
|
|
return .complete()
|
|
}
|
|
} else {
|
|
return .complete()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|> deliverOnMainQueue
|
|
|
|
let disposable = MetaDisposable()
|
|
disposable.set((signal
|
|
|> afterDisposed { [weak disposable] in
|
|
Queue.mainQueue().async {
|
|
if let disposable = disposable {
|
|
self.replyFromNotificationsDisposables.remove(disposable)
|
|
}
|
|
completionHandler()
|
|
}
|
|
}).start())
|
|
self.replyFromNotificationsDisposables.add(disposable)
|
|
} else {
|
|
completionHandler()
|
|
}
|
|
})
|
|
}
|
|
|
|
func requestNotificationTokenInvalidation() {
|
|
UIApplication.shared.unregisterForRemoteNotifications()
|
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
})
|
|
}
|
|
|
|
private func registerForNotifications(context: AccountContextImpl, authorize: Bool = true, completion: @escaping (Bool) -> Void = { _ in }) {
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let _ = (context.sharedContext.accountManager.transaction { transaction -> Bool in
|
|
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.inAppNotificationSettings)?.get(InAppNotificationSettings.self) ?? InAppNotificationSettings.defaultSettings
|
|
return settings.displayNameOnLockscreen
|
|
}
|
|
|> deliverOnMainQueue).start(next: { displayNames in
|
|
self.registerForNotifications(replyString: presentationData.strings.Notification_Reply, messagePlaceholderString: presentationData.strings.Conversation_InputTextPlaceholder, hiddenContentString: presentationData.strings.Watch_MessageView_Title, hiddenReactionContentString: presentationData.strings.Notification_LockScreenReactionPlaceholder, hiddenStoryContentString: presentationData.strings.Notification_LockScreenStoryPlaceholder, hiddenStoryReactionContentString: presentationData.strings.PUSH_REACT_STORY_HIDDEN, includeNames: displayNames, authorize: authorize, completion: completion)
|
|
})
|
|
}
|
|
|
|
private func registerForNotifications(replyString: String, messagePlaceholderString: String, hiddenContentString: String, hiddenReactionContentString: String, hiddenStoryContentString: String, hiddenStoryReactionContentString: String, includeNames: Bool, authorize: Bool = true, completion: @escaping (Bool) -> Void = { _ in }) {
|
|
let notificationCenter = UNUserNotificationCenter.current()
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: get settings (authorize: \(authorize))")
|
|
notificationCenter.getNotificationSettings(completionHandler: { settings in
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: received settings: \(settings.authorizationStatus)")
|
|
|
|
switch (settings.authorizationStatus, authorize) {
|
|
case (.authorized, _), (.notDetermined, true):
|
|
var authorizationOptions: UNAuthorizationOptions = [.badge, .sound, .alert, .carPlay]
|
|
if #available(iOS 12.0, *) {
|
|
authorizationOptions.insert(.providesAppNotificationSettings)
|
|
}
|
|
if #available(iOS 13.0, *) {
|
|
authorizationOptions.insert(.announcement)
|
|
}
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: request authorization")
|
|
notificationCenter.requestAuthorization(options: authorizationOptions, completionHandler: { result, _ in
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: received authorization: \(result)")
|
|
completion(result)
|
|
if result {
|
|
Queue.mainQueue().async {
|
|
let reply = UNTextInputNotificationAction(identifier: "reply", title: replyString, options: [], textInputButtonTitle: replyString, textInputPlaceholder: messagePlaceholderString)
|
|
|
|
let unknownMessageCategory: UNNotificationCategory
|
|
let repliableMessageCategory: UNNotificationCategory
|
|
let repliableMediaMessageCategory: UNNotificationCategory
|
|
let groupRepliableMessageCategory: UNNotificationCategory
|
|
let groupRepliableMediaMessageCategory: UNNotificationCategory
|
|
let channelMessageCategory: UNNotificationCategory
|
|
let reactionMessageCategory: UNNotificationCategory
|
|
let storyCategory: UNNotificationCategory
|
|
let storyReactionCategory: UNNotificationCategory
|
|
|
|
var options: UNNotificationCategoryOptions = []
|
|
if includeNames {
|
|
options.insert(.hiddenPreviewsShowTitle)
|
|
}
|
|
|
|
var carPlayOptions = options
|
|
carPlayOptions.insert(.allowInCarPlay)
|
|
if #available(iOS 13.2, *) {
|
|
carPlayOptions.insert(.allowAnnouncement)
|
|
}
|
|
|
|
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
repliableMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
|
repliableMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
|
groupRepliableMessageCategory = UNNotificationCategory(identifier: "gr", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
groupRepliableMediaMessageCategory = UNNotificationCategory(identifier: "gm", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
channelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
reactionMessageCategory = UNNotificationCategory(identifier: "t", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenReactionContentString, options: options)
|
|
storyCategory = UNNotificationCategory(identifier: "st", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenStoryContentString, options: options)
|
|
storyReactionCategory = UNNotificationCategory(identifier: "str", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenStoryReactionContentString, options: options)
|
|
|
|
UNUserNotificationCenter.current().setNotificationCategories([
|
|
unknownMessageCategory,
|
|
repliableMessageCategory,
|
|
repliableMediaMessageCategory,
|
|
channelMessageCategory,
|
|
reactionMessageCategory,
|
|
groupRepliableMessageCategory,
|
|
groupRepliableMediaMessageCategory,
|
|
storyCategory,
|
|
storyReactionCategory
|
|
])
|
|
|
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: invoke registerForRemoteNotifications")
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
}
|
|
}
|
|
})
|
|
default:
|
|
break
|
|
}
|
|
})
|
|
}
|
|
|
|
@available(iOS 10.0, *)
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
|
let _ = (accountIdFromNotification(notification, sharedContext: self.sharedContextPromise.get())
|
|
|> deliverOnMainQueue).start(next: { accountId in
|
|
if let context = self.contextValue {
|
|
if let accountId = accountId, context.context.account.id != accountId {
|
|
completionHandler([.alert])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
@available(iOS 12.0, *)
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
|
self.openNotificationSettingsWhenReady()
|
|
}
|
|
|
|
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
|
|
Logger.shared.log("App \(self.episodeId)", "handleEventsForBackgroundURLSession \(identifier)")
|
|
completionHandler()
|
|
}
|
|
|
|
private var lastCheckForUpdatesTimestamp: Double?
|
|
private let currentCheckForUpdatesDisposable = MetaDisposable()
|
|
|
|
private func maybeCheckForUpdates() {
|
|
#if targetEnvironment(simulator)
|
|
#else
|
|
guard let buildConfig = self.buildConfig, !buildConfig.isAppStoreBuild, let appCenterId = buildConfig.appCenterId, !appCenterId.isEmpty else {
|
|
return
|
|
}
|
|
let timestamp = CFAbsoluteTimeGetCurrent()
|
|
if self.lastCheckForUpdatesTimestamp == nil || self.lastCheckForUpdatesTimestamp! < timestamp - 10.0 * 60.0 {
|
|
self.lastCheckForUpdatesTimestamp = timestamp
|
|
|
|
if let url = URL(string: "https://api.appcenter.ms/v0.1/public/sdk/apps/\(appCenterId)/releases/latest") {
|
|
self.currentCheckForUpdatesDisposable.set((downloadHTTPData(url: url)
|
|
|> deliverOnMainQueue).start(next: { [weak self] data in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else {
|
|
return
|
|
}
|
|
guard let dict = json as? [String: Any] else {
|
|
return
|
|
}
|
|
guard let versionString = dict["version"] as? String, let version = Int(versionString) else {
|
|
return
|
|
}
|
|
guard let releaseNotesUrl = dict["release_notes_url"] as? String else {
|
|
return
|
|
}
|
|
guard let currentVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, let currentVersion = Int(currentVersionString) else {
|
|
return
|
|
}
|
|
if currentVersion < version {
|
|
let _ = (strongSelf.sharedContextPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { sharedContext in
|
|
let presentationData = sharedContext.sharedContext.currentPresentationData.with { $0 }
|
|
sharedContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "A new build is available", actions: [
|
|
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
|
|
TextAlertAction(type: .defaultAction, title: "Show", action: {
|
|
sharedContext.sharedContext.applicationBindings.openUrl(releaseNotesUrl)
|
|
})
|
|
]), on: .root, blockInteraction: false, completion: {})
|
|
})
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
override var next: UIResponder? {
|
|
if let context = self.contextValue, let controller = context.context.keyShortcutsController {
|
|
return controller
|
|
}
|
|
return super.next
|
|
}
|
|
|
|
@objc func debugPressed() {
|
|
let _ = (Logger.shared.collectShortLogFiles()
|
|
|> deliverOnMainQueue).start(next: { logs in
|
|
var activityItems: [Any] = []
|
|
for (_, path) in logs {
|
|
activityItems.append(URL(fileURLWithPath: path))
|
|
}
|
|
|
|
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
|
|
|
|
self.window?.rootViewController?.present(activityController, animated: true, completion: nil)
|
|
})
|
|
}
|
|
|
|
private func resetIntentsIfNeeded(context: AccountContextImpl) {
|
|
let _ = (context.sharedContext.accountManager.transaction { transaction in
|
|
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.intentsSettings)?.get(IntentsSettings.self) ?? IntentsSettings.defaultSettings
|
|
if !settings.initiallyReset || settings.account == nil {
|
|
if #available(iOS 10.0, *) {
|
|
Queue.mainQueue().async {
|
|
INInteraction.deleteAll()
|
|
}
|
|
}
|
|
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.intentsSettings, { _ in
|
|
return PreferencesEntry(IntentsSettings(initiallyReset: true, account: context.account.peerId, contacts: settings.contacts, privateChats: settings.privateChats, savedMessages: settings.savedMessages, groups: settings.groups, onlyShared: settings.onlyShared))
|
|
})
|
|
}
|
|
}).start()
|
|
}
|
|
}
|
|
|
|
private func notificationPayloadKey(data: Data) -> Data? {
|
|
if data.count < 8 {
|
|
return nil
|
|
}
|
|
return data.subdata(in: 0 ..< 8)
|
|
}
|
|
|
|
@available(iOS 10.0, *)
|
|
private func accountIdFromNotification(_ notification: UNNotification, sharedContext: Signal<SharedApplicationContext, NoError>) -> Signal<AccountRecordId?, NoError> {
|
|
if let id = notification.request.content.userInfo["accountId"] as? Int64 {
|
|
return .single(AccountRecordId(rawValue: id))
|
|
} else if let idString = notification.request.content.userInfo["accountId"] as? String, let id = Int64(idString) {
|
|
return .single(AccountRecordId(rawValue: id))
|
|
} else {
|
|
var encryptedData: Data?
|
|
if var encryptedPayload = notification.request.content.userInfo["p"] as? String {
|
|
encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+")
|
|
encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/")
|
|
while encryptedPayload.count % 4 != 0 {
|
|
encryptedPayload.append("=")
|
|
}
|
|
encryptedData = Data(base64Encoded: encryptedPayload)
|
|
}
|
|
if let encryptedData = encryptedData, let notificationKeyId = notificationPayloadKey(data: encryptedData) {
|
|
return sharedContext
|
|
|> take(1)
|
|
|> mapToSignal { sharedContext -> Signal<AccountRecordId?, NoError> in
|
|
return sharedContext.sharedContext.activeAccountContexts
|
|
|> take(1)
|
|
|> mapToSignal { _, contexts, _ -> Signal<AccountRecordId?, NoError> in
|
|
let keys = contexts.map { _, context, _ -> Signal<(AccountRecordId, MasterNotificationKey)?, NoError> in
|
|
return masterNotificationsKey(account: context.account, ignoreDisabled: true)
|
|
|> map { key in
|
|
return (context.account.id, key)
|
|
}
|
|
}
|
|
return combineLatest(keys)
|
|
|> map { keys -> AccountRecordId? in
|
|
for idAndKey in keys {
|
|
if let (id, key) = idAndKey, key.id == notificationKeyId {
|
|
return id
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
} else if let userId = notification.request.content.userInfo["userId"] as? Int {
|
|
return sharedContext
|
|
|> take(1)
|
|
|> mapToSignal { sharedContext -> Signal<AccountRecordId?, NoError> in
|
|
return sharedContext.sharedContext.activeAccountContexts
|
|
|> take(1)
|
|
|> map { _, contexts, _ -> AccountRecordId? in
|
|
for (_, context, _) in contexts {
|
|
if Int(context.account.peerId.id._internalGetInt64Value()) == userId {
|
|
return context.account.id
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 10.0, *)
|
|
private func peerIdFromNotification(_ notification: UNNotification) -> (peerId: PeerId, threadId: Int64?)? {
|
|
let threadId = notification.request.content.userInfo["threadId"] as? Int64
|
|
|
|
if let peerId = notification.request.content.userInfo["peerId"] as? Int64 {
|
|
return (PeerId(peerId), threadId)
|
|
} else if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerId = Int64(peerIdString) {
|
|
return (PeerId(peerId), threadId)
|
|
} else {
|
|
let payload = notification.request.content.userInfo
|
|
var peerId: PeerId?
|
|
if let fromId = payload["from_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
|
|
} else if let fromId = payload["chat_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
|
|
} else if let fromId = payload["channel_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
|
|
} else if let fromId = payload["encryption_id"] {
|
|
let fromIdValue = fromId as! NSString
|
|
peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
|
|
}
|
|
|
|
if let peerId = peerId {
|
|
return (peerId, threadId)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private func messageIdFromNotification(peerId: PeerId, notification: UNNotification) -> MessageId? {
|
|
let payload = notification.request.content.userInfo
|
|
if let messageIdNamespace = payload["messageId.namespace"] as? Int32, let messageIdId = payload["messageId.id"] as? Int32 {
|
|
return MessageId(peerId: peerId, namespace: messageIdNamespace, id: messageIdId)
|
|
}
|
|
|
|
if let msgId = payload["msg_id"] {
|
|
let msgIdValue = msgId as! NSString
|
|
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func storyIdFromNotification(peerId: PeerId, notification: UNNotification) -> StoryId? {
|
|
let payload = notification.request.content.userInfo
|
|
if let storyId = payload["story_id"] {
|
|
let storyIdValue = storyId as! NSString
|
|
return StoryId(peerId: peerId, id: Int32(storyIdValue.intValue))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private enum DownloadFileError {
|
|
case network
|
|
}
|
|
|
|
private func downloadHTTPData(url: URL) -> Signal<Data, DownloadFileError> {
|
|
return Signal { subscriber in
|
|
let completed = Atomic<Bool>(value: false)
|
|
let downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { location, _, error in
|
|
let _ = completed.swap(true)
|
|
if let location = location, let data = try? Data(contentsOf: location) {
|
|
subscriber.putNext(data)
|
|
subscriber.putCompletion()
|
|
} else {
|
|
subscriber.putError(.network)
|
|
}
|
|
})
|
|
downloadTask.resume()
|
|
|
|
return ActionDisposable {
|
|
if !completed.with({ $0 }) {
|
|
downloadTask.cancel()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getMemoryConsumption() -> Int {
|
|
guard let memory_offset = MemoryLayout.offset(of: \task_vm_info_data_t.min_address) else {
|
|
return 0
|
|
}
|
|
let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<integer_t>.size)
|
|
let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(memory_offset / MemoryLayout<integer_t>.size)
|
|
var info = task_vm_info_data_t()
|
|
var count = TASK_VM_INFO_COUNT
|
|
let kr = withUnsafeMutablePointer(to: &info) { infoPtr in
|
|
infoPtr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in
|
|
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count)
|
|
}
|
|
}
|
|
guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else {
|
|
return 0
|
|
}
|
|
return Int(info.phys_footprint)
|
|
}
|