Swiftgram/Telegram-iOS/ApplicationContext.swift
2018-11-27 21:31:06 +04:00

1078 lines
57 KiB
Swift

import Foundation
import Intents
import TelegramUI
import SwiftSignalKit
import Postbox
import TelegramCore
import Display
import LegacyComponents
func applicationContext(networkArguments: NetworkInitializationArguments, applicationBindings: TelegramApplicationBindings, replyFromNotificationsActive: Signal<Bool, NoError>, backgroundAudioActive: Signal<Bool, NoError>, watchManagerArguments: Signal<WatchManagerArguments?, NoError>, accountManager: AccountManager, rootPath: String, legacyBasePath: String, testingEnvironment: Bool, mainWindow: Window1, reinitializedNotificationSettings: @escaping () -> Void) -> Signal<ApplicationContext?, NoError> {
return currentAccount(allocateIfNotExists: true, networkArguments: networkArguments, supplementary: false, manager: accountManager, rootPath: rootPath, beginWithTestingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods)
|> filter { $0 != nil }
|> deliverOnMainQueue
|> mapToSignal { account -> Signal<ApplicationContext?, NoError> in
if let account = account {
switch account {
case .upgrading:
return .single(.upgrading(UpgradingApplicationContext()))
case let .unauthorized(account):
return currentPresentationDataAndSettings(postbox: account.postbox)
|> deliverOnMainQueue
|> map { dataAndSettings -> ApplicationContext? in
return .unauthorized(UnauthorizedApplicationContext(applicationContext: TelegramApplicationContext(applicationBindings: applicationBindings, accountManager: accountManager, account: nil, initialPresentationDataAndSettings: dataAndSettings, postbox: account.postbox), account: account))
}
case let .authorized(account):
return currentPresentationDataAndSettings(postbox: account.postbox)
|> deliverOnMainQueue
|> map { dataAndSettings -> ApplicationContext? in
return .authorized(AuthorizedApplicationContext(mainWindow: mainWindow, applicationContext: TelegramApplicationContext(applicationBindings: applicationBindings, accountManager: accountManager, account: account, initialPresentationDataAndSettings: dataAndSettings, postbox: account.postbox), replyFromNotificationsActive: replyFromNotificationsActive, backgroundAudioActive: backgroundAudioActive, watchManagerArguments: watchManagerArguments, account: account, accountManager: accountManager, legacyBasePath: legacyBasePath, showCallsTab: dataAndSettings.callListSettings.showTab, reinitializedNotificationSettings: reinitializedNotificationSettings))
}
}
} else {
return .single(nil)
}
}
}
func isAccessLocked(data: PostboxAccessChallengeData, at timestamp: Int32) -> Bool {
if data.isLockable, let autolockDeadline = data.autolockDeadline, autolockDeadline <= timestamp {
return true
} else {
return false
}
}
enum ApplicationContext {
case upgrading(UpgradingApplicationContext)
case unauthorized(UnauthorizedApplicationContext)
case authorized(AuthorizedApplicationContext)
var account: Account? {
switch self {
case .upgrading:
return nil
case .unauthorized:
return nil
case let .authorized(context):
return context.account
}
}
var accountId: AccountRecordId? {
switch self {
case .upgrading:
return nil
case let .unauthorized(unauthorized):
return unauthorized.account.id
case let .authorized(authorized):
return authorized.account.id
}
}
var rootController: NavigationController {
switch self {
case let .upgrading(context):
return context.rootController
case let .unauthorized(context):
return context.rootController
case let .authorized(context):
return context.rootController
}
}
var overlayControllers: [ViewController] {
switch self {
case .upgrading:
return []
case .unauthorized:
return []
case let .authorized(context):
return [context.overlayMediaController, context.notificationController]
}
}
}
final class UpgradingApplicationContext {
let rootController: NavigationController
init() {
self.rootController = NavigationController(mode: .single, theme: NavigationControllerTheme(navigationBar: NavigationBarTheme(buttonColor: .white, disabledButtonColor: .gray, primaryTextColor: .white, backgroundColor: .black, separatorColor: .white, badgeBackgroundColor: .black, badgeStrokeColor: .black, badgeTextColor: .white), emptyAreaColor: .black, emptyDetailIcon: nil))
let noticeController = ViewController(navigationBarPresentationData: nil)
self.rootController.pushViewController(noticeController, animated: false)
}
}
final class UnauthorizedApplicationContext {
let applicationContext: TelegramApplicationContext
let account: UnauthorizedAccount
let rootController: AuthorizationSequenceController
init(applicationContext: TelegramApplicationContext, account: UnauthorizedAccount) {
self.account = account
self.applicationContext = applicationContext
self.rootController = AuthorizationSequenceController(account: account, strings: (applicationContext.currentPresentationData.with { $0 }).strings, openUrl: { [weak applicationContext] url in
applicationContext?.applicationBindings.openUrl(url)
}, apiId: BuildConfig.shared().apiId, apiHash: BuildConfig.shared().apiHash)
account.shouldBeServiceTaskMaster.set(applicationContext.applicationBindings.applicationInForeground |> map { value -> AccountServiceTaskMasterMode in
if value {
return .always
} else {
return .never
}
})
}
}
private struct PasscodeState: Equatable {
let isActive: Bool
let challengeData: PostboxAccessChallengeData
let autolockTimeout: Int32?
let enableBiometrics: Bool
}
private enum CallStatusText: Equatable {
case none
case inProgress(Double?)
static func ==(lhs: CallStatusText, rhs: CallStatusText) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .inProgress(lhsReferenceTime):
if case let .inProgress(rhsReferenceTime) = rhs, lhsReferenceTime == rhsReferenceTime {
return true
} else {
return false
}
}
}
}
final class AuthorizedApplicationContext {
let mainWindow: Window1
let lockedCoveringView: LockedWindowCoveringView
let applicationContext: TelegramApplicationContext
let account: Account
let replyFromNotificationsActive: Signal<Bool, NoError>
let backgroundAudioActive: Signal<Bool, NoError>
let rootController: TelegramRootController
let overlayMediaController: OverlayMediaController
let notificationController: NotificationContainerController
private var scheduledOperChatWithPeerId: PeerId?
private var scheduledOpenExternalUrl: URL?
let wakeupManager: WakeupManager
let notificationManager: NotificationManager
private let passcodeStatusDisposable = MetaDisposable()
private let passcodeLockDisposable = MetaDisposable()
private let loggedOutDisposable = MetaDisposable()
private let inAppNotificationSettingsDisposable = MetaDisposable()
private let notificationMessagesDisposable = MetaDisposable()
private let termsOfServiceUpdatesDisposable = MetaDisposable()
private let proccedTOSBotDisposable = MetaDisposable()
private var watchNavigateToMessageDisposable = MetaDisposable()
private var inAppNotificationSettings: InAppNotificationSettings?
private var isLocked: Bool = true
private var passcodeController: ViewController?
private var callController: CallController?
private let hasOngoingCall = ValuePromise<Bool>(false)
private let callState = Promise<PresentationCallState?>(nil)
private var currentTermsOfServiceUpdate: TermsOfServiceUpdate?
private var currentTermsOfServiceUpdateController: TermsOfServiceController?
private let unlockedStatePromise = Promise<Bool>()
var unlockedState: Signal<Bool, NoError> {
return self.unlockedStatePromise.get()
}
var applicationBadge: Signal<Int32, NoError> {
return renderedTotalUnreadCount(postbox: self.account.postbox)
|> map {
$0.0
}
}
private var presentationDataDisposable: Disposable?
private var displayAlertsDisposable: Disposable?
private var removeNotificationsDisposable: Disposable?
private var callDisposable: Disposable?
private var callStateDisposable: Disposable?
private var currentCallStatusText: CallStatusText = .none
private var currentCallStatusTextTimer: SwiftSignalKit.Timer?
private var applicationInForegroundDisposable: Disposable?
private var showCallsTab: Bool
private var showCallsTabDisposable: Disposable?
private var enablePostboxTransactionsDiposable: Disposable?
init(mainWindow: Window1, applicationContext: TelegramApplicationContext, replyFromNotificationsActive: Signal<Bool, NoError>, backgroundAudioActive: Signal<Bool, NoError>, watchManagerArguments: Signal<WatchManagerArguments?, NoError>, account: Account, accountManager: AccountManager, legacyBasePath: String, showCallsTab: Bool, reinitializedNotificationSettings: @escaping () -> Void) {
setupLegacyComponents(account: account)
let presentationData = applicationContext.currentPresentationData.with { $0 }
self.mainWindow = mainWindow
self.lockedCoveringView = LockedWindowCoveringView(theme: presentationData.theme)
self.applicationContext = applicationContext
self.account = account
self.replyFromNotificationsActive = replyFromNotificationsActive
self.backgroundAudioActive = backgroundAudioActive
let runningBackgroundLocationTasks: Signal<Bool, NoError>
if let liveLocationManager = applicationContext.liveLocationManager {
runningBackgroundLocationTasks = liveLocationManager.isPolling
} else {
runningBackgroundLocationTasks = .single(false)
}
let runningWatchTasksPromise = Promise<WatchRunningTasks?>(nil)
self.wakeupManager = WakeupManager(inForeground: applicationContext.applicationBindings.applicationInForeground, runningServiceTasks: account.importantTasksRunning, runningBackgroundLocationTasks: runningBackgroundLocationTasks, runningWatchTasks: runningWatchTasksPromise.get())
self.wakeupManager.account = account
self.showCallsTab = showCallsTab
self.notificationManager = NotificationManager()
self.notificationManager.account = account
self.notificationManager.isApplicationInForeground = false
self.overlayMediaController = OverlayMediaController()
applicationContext.attachOverlayMediaController(self.overlayMediaController)
var presentImpl: ((ViewController, Any?) -> Void)?
var openSettingsImpl: (() -> Void)?
let callManager = PresentationCallManager(account: account, getDeviceAccessData: {
return (account.telegramApplicationContext.currentPresentationData.with { $0 }, { c, a in
presentImpl?(c, a)
}, {
openSettingsImpl?()
})
}, networkType: account.networkType, audioSession: applicationContext.mediaManager!.audioSession, callSessionManager: account.callSessionManager)
applicationContext.callManager = callManager
applicationContext.hasOngoingCall = self.hasOngoingCall.get()
let shouldBeServiceTaskMaster = combineLatest(applicationContext.applicationBindings.applicationInForeground, self.wakeupManager.isWokenUp, replyFromNotificationsActive, backgroundAudioActive, callManager.hasActiveCalls)
|> map { foreground, wokenUp, replyFromNotificationsActive, backgroundAudioActive, hasActiveCalls -> AccountServiceTaskMasterMode in
if foreground || wokenUp || replyFromNotificationsActive || hasActiveCalls {
return .always
} else {
return .never
}
}
account.shouldBeServiceTaskMaster.set(shouldBeServiceTaskMaster)
self.enablePostboxTransactionsDiposable = (combineLatest(shouldBeServiceTaskMaster, backgroundAudioActive)
|> map { shouldBeServiceTaskMaster, backgroundAudioActive -> Bool in
switch shouldBeServiceTaskMaster {
case .never:
break
default:
return true
}
if backgroundAudioActive {
return true
}
return false
}
|> deliverOnMainQueue).start(next: { [weak account] next in
if let account = account {
Logger.shared.log("ApplicationContext", "setting canBeginTransactions to \(next)")
account.postbox.setCanBeginTransactions(next)
}
})
account.shouldExplicitelyKeepWorkerConnections.set(backgroundAudioActive)
account.shouldKeepOnlinePresence.set(applicationContext.applicationBindings.applicationInForeground)
let cache = TGCache(cachesPath: legacyBasePath + "/Caches")!
setupAccount(account, fetchCachedResourceRepresentation: fetchCachedResourceRepresentation, transformOutgoingMessageMedia: transformOutgoingMessageMedia, preFetchedResourcePath: { resource in
preFetchedLegacyResourcePath(basePath: legacyBasePath, resource: resource, cache: cache)
})
account.applicationContext = applicationContext
self.notificationController = NotificationContainerController(account: account)
self.mainWindow.previewThemeAccentColor = presentationData.theme.rootController.navigationBar.accentTextColor
self.mainWindow.previewThemeDarkBlur = presentationData.theme.chatList.searchBarKeyboardColor == .dark
self.rootController = TelegramRootController(account: account)
if KeyShortcutsController.isAvailable {
let keyShortcutsController = KeyShortcutsController { [weak self] f in
if let strongSelf = self {
if let tabController = strongSelf.rootController.rootTabController {
let controller = tabController.controllers[tabController.selectedIndex]
if !f(controller) {
return
}
if let controller = strongSelf.rootController.topViewController as? ViewController {
if !f(controller) {
return
}
}
}
strongSelf.mainWindow.forEachViewController(f)
}
}
applicationContext.keyShortcutsController = keyShortcutsController
}
self.applicationInForegroundDisposable = applicationContext.applicationBindings.applicationInForeground.start(next: { [weak self] value in
Queue.mainQueue().async {
self?.notificationManager.isApplicationInForeground = value
}
})
self.mainWindow.inCallNavigate = { [weak self] in
if let strongSelf = self, let callController = strongSelf.callController {
if callController.isNodeLoaded && callController.view.superview == nil {
strongSelf.rootController.view.endEditing(true)
strongSelf.mainWindow.present(callController, on: .calls)
}
}
}
applicationContext.presentGlobalController = { [weak self] c, a in
self?.mainWindow.present(c, on: .root)
}
applicationContext.presentCrossfadeController = { [weak self] in
guard let strongSelf = self else {
return
}
var exists = false
strongSelf.mainWindow.forEachViewController { controller in
if controller is ThemeSettingsCrossfadeController {
exists = true
}
return true
}
if !exists {
mainWindow.present(ThemeSettingsCrossfadeController(), on: .root)
}
}
applicationContext.navigateToCurrentCall = { [weak self] in
if let strongSelf = self, let callController = strongSelf.callController {
if callController.isNodeLoaded && callController.view.superview == nil {
strongSelf.rootController.view.endEditing(true)
strongSelf.mainWindow.present(callController, on: .calls)
}
}
}
presentImpl = { [weak self] c, _ in
self?.mainWindow.present(c, on: .root)
}
openSettingsImpl = {
applicationContext.applicationBindings.openSettings()
}
let previousPasscodeState = Atomic<PasscodeState?>(value: nil)
let preferencesKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([ApplicationSpecificPreferencesKeys.presentationPasscodeSettings]))
self.passcodeStatusDisposable.set((combineLatest(queue: Queue.mainQueue(), account.postbox.combinedView(keys: [.accessChallengeData, preferencesKey]), applicationContext.applicationBindings.applicationIsActive)
|> map { view, isActive -> (PostboxAccessChallengeData, PresentationPasscodeSettings?, Bool) in
let accessChallengeData = (view.views[.accessChallengeData] as? AccessChallengeDataView)?.data ?? PostboxAccessChallengeData.none
let passcodeSettings = (view.views[preferencesKey] as! PreferencesView).values[ApplicationSpecificPreferencesKeys.presentationPasscodeSettings] as? PresentationPasscodeSettings
return (accessChallengeData, passcodeSettings, isActive)
}
|> map { accessChallengeData, passcodeSettings, isActive -> PasscodeState in
return PasscodeState(isActive: isActive, challengeData: accessChallengeData, autolockTimeout: passcodeSettings?.autolockTimeout, enableBiometrics: passcodeSettings?.enableBiometrics ?? false)
}).start(next: { [weak self] updatedState in
guard let strongSelf = self else {
return
}
let previousState = previousPasscodeState.swap(updatedState)
var updatedAutolockDeadline: Int32?
if updatedState.isActive != previousState?.isActive, let autolockTimeout = updatedState.autolockTimeout {
updatedAutolockDeadline = Int32(CFAbsoluteTimeGetCurrent()) + max(10, autolockTimeout)
}
var effectiveAutolockDeadline = updatedState.challengeData.autolockDeadline
if updatedState.isActive {
} else if previousState != nil && previousState!.autolockTimeout != updatedState.autolockTimeout {
effectiveAutolockDeadline = updatedAutolockDeadline
}
if let previousState = previousState, previousState.isActive, !updatedState.isActive, effectiveAutolockDeadline != 0 {
effectiveAutolockDeadline = updatedAutolockDeadline
}
var isLocked = false
if isAccessLocked(data: updatedState.challengeData.withUpdatedAutolockDeadline(effectiveAutolockDeadline), at: Int32(CFAbsoluteTimeGetCurrent())) {
isLocked = true
updatedAutolockDeadline = 0
}
let isLockable: Bool
switch updatedState.challengeData {
case .none:
isLockable = false
default:
isLockable = true
}
if previousState?.isActive != updatedState.isActive || isLocked != strongSelf.isLocked {
if updatedAutolockDeadline != previousState?.challengeData.autolockDeadline {
let _ = (account.postbox.transaction { transaction -> Void in
let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(updatedAutolockDeadline)
transaction.setAccessChallengeData(data)
}).start()
}
strongSelf.isLocked = isLocked
strongSelf.notificationManager.isApplicationLocked = isLocked
if isLocked {
if updatedState.isActive {
if strongSelf.passcodeController == nil {
var attemptData: TGPasscodeEntryAttemptData?
if let attempts = updatedState.challengeData.attempts {
attemptData = TGPasscodeEntryAttemptData(numberOfInvalidAttempts: Int(attempts.count), dateOfLastInvalidAttempt: Double(attempts.timestamp))
}
var mode: TGPasscodeEntryControllerMode
switch updatedState.challengeData {
case .none:
mode = TGPasscodeEntryControllerModeVerifySimple
case .numericalPassword:
mode = TGPasscodeEntryControllerModeVerifySimple
case .plaintextPassword:
mode = TGPasscodeEntryControllerModeVerifyComplex
}
let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }
let presentAnimated = previousState != nil && previousState!.isActive
let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: presentAnimated), theme: presentationData.theme)
let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: mode, cancelEnabled: false, allowTouchId: updatedState.enableBiometrics, attemptData: attemptData, completion: { value in
if value != nil {
let _ = (account.postbox.transaction { transaction -> Void in
let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(nil)
transaction.setAccessChallengeData(data)
}).start()
}
})!
controller.checkCurrentPasscode = { value in
if let value = value {
switch updatedState.challengeData {
case .none:
return true
case let .numericalPassword(code, _, _):
return value == code
case let .plaintextPassword(code, _, _):
return value == code
}
} else {
return false
}
}
controller.updateAttemptData = { attemptData in
let _ = account.postbox.transaction({ transaction -> Void in
var attempts: AccessChallengeAttempts?
if let attemptData = attemptData {
attempts = AccessChallengeAttempts(count: Int32(attemptData.numberOfInvalidAttempts), timestamp: Int32(attemptData.dateOfLastInvalidAttempt))
}
var data = transaction.getAccessChallengeData()
switch data {
case .none:
break
case let .numericalPassword(value, timeout, _):
data = .numericalPassword(value: value, timeout: timeout, attempts: attempts)
case let .plaintextPassword(value, timeout, _):
data = .plaintextPassword(value: value, timeout: timeout, attempts: attempts)
}
transaction.setAccessChallengeData(data)
}).start()
}
controller.touchIdCompletion = {
let _ = (account.postbox.transaction { transaction -> Void in
let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(nil)
transaction.setAccessChallengeData(data)
}).start()
}
legacyController.bind(controller: controller)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
legacyController.statusBar.statusBarStyle = .White
strongSelf.passcodeController = legacyController
strongSelf.unlockedStatePromise.set(.single(false))
if presentAnimated {
legacyController.presentationCompleted = {
if let strongSelf = self {
strongSelf.rootController.view.isHidden = true
strongSelf.overlayMediaController.view.isHidden = true
strongSelf.notificationController.view.isHidden = true
}
}
} else {
strongSelf.rootController.view.isHidden = true
strongSelf.overlayMediaController.view.isHidden = true
strongSelf.notificationController.view.isHidden = true
}
strongSelf.mainWindow.present(legacyController, on: .root)
if !presentAnimated {
controller.refreshTouchId()
}
} else if previousState?.isActive != updatedState.isActive, updatedState.isActive, let passcodeController = strongSelf.passcodeController as? LegacyController {
if let controller = passcodeController.legacyController as? TGPasscodeEntryController {
controller.refreshTouchId()
}
}
strongSelf.updateCoveringViewSnaphot(false)
strongSelf.mainWindow.coveringView = nil
} else {
strongSelf.unlockedStatePromise.set(.single(false))
strongSelf.updateCoveringViewSnaphot(true)
strongSelf.mainWindow.coveringView = strongSelf.lockedCoveringView
strongSelf.rootController.view.isHidden = true
strongSelf.overlayMediaController.view.isHidden = true
strongSelf.notificationController.view.isHidden = true
}
} else {
if !updatedState.isActive && updatedState.autolockTimeout != nil && isLockable {
strongSelf.updateCoveringViewSnaphot(true)
strongSelf.mainWindow.coveringView = strongSelf.lockedCoveringView
strongSelf.rootController.view.isHidden = true
strongSelf.overlayMediaController.view.isHidden = true
strongSelf.notificationController.view.isHidden = true
} else {
strongSelf.updateCoveringViewSnaphot(false)
strongSelf.mainWindow.coveringView = nil
strongSelf.rootController.view.isHidden = false
strongSelf.overlayMediaController.view.isHidden = false
strongSelf.notificationController.view.isHidden = false
if strongSelf.rootController.rootTabController == nil {
strongSelf.rootController.addRootControllers(showCallsTab: strongSelf.showCallsTab)
if let peerId = strongSelf.scheduledOperChatWithPeerId {
strongSelf.scheduledOperChatWithPeerId = nil
strongSelf.openChatWithPeerId(peerId: peerId)
}
if let url = strongSelf.scheduledOpenExternalUrl {
strongSelf.scheduledOpenExternalUrl = nil
strongSelf.openUrl(url)
}
DeviceAccess.authorizeAccess(to: .contacts, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, present: { c, a in
}, openSettings: {
}, { _ in
})
if #available(iOS 10.0, *) {
INPreferences.requestSiriAuthorization { _ in
}
}
if #available(iOS 12.0, *) {
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
userActivity.interaction?.donate(completion: { _ in
})
}
if let passcodeController = strongSelf.passcodeController {
if let chatListController = strongSelf.rootController.chatListController {
let _ = chatListController.ready.get().start(next: { [weak passcodeController] _ in
if let strongSelf = self, let passcodeController = passcodeController, strongSelf.passcodeController === passcodeController {
strongSelf.passcodeController = nil
strongSelf.rootController.chatListController?.displayNode.recursivelyEnsureDisplaySynchronously(true)
passcodeController.dismiss()
}
})
} else {
strongSelf.passcodeController = nil
strongSelf.rootController.chatListController?.displayNode.recursivelyEnsureDisplaySynchronously(true)
passcodeController.dismiss()
}
}
} else {
if let passcodeController = strongSelf.passcodeController {
strongSelf.passcodeController = nil
passcodeController.dismiss()
}
}
}
strongSelf.unlockedStatePromise.set(.single(true))
}
}/* else if updatedAutolockDeadline != previousState?.challengeData.autolockDeadline {
let _ = (account.postbox.transaction { transaction -> Void in
let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(updatedAutolockDeadline)
transaction.setAccessChallengeData(data)
}).start()
}*/
}))
let accountId = account.id
self.loggedOutDisposable.set(account.loggedOut.start(next: { value in
if value {
Logger.shared.log("ApplicationContext", "account logged out")
let _ = logoutFromAccount(id: accountId, accountManager: accountManager).start()
}
}))
let inAppPreferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.inAppNotificationSettings]))
self.inAppNotificationSettingsDisposable.set(((account.postbox.combinedView(keys: [inAppPreferencesKey])) |> deliverOnMainQueue).start(next: { [weak self] views in
if let strongSelf = self {
if let view = views.views[inAppPreferencesKey] as? PreferencesView {
if let settings = view.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings {
let previousSettings = strongSelf.inAppNotificationSettings
strongSelf.inAppNotificationSettings = settings
if let previousSettings = previousSettings, previousSettings.displayNameOnLockscreen != settings.displayNameOnLockscreen {
reinitializedNotificationSettings()
}
}
}
}
}))
self.notificationMessagesDisposable.set((account.stateManager.notificationMessages |> deliverOn(Queue.mainQueue())).start(next: { [weak self] messageList in
if let strongSelf = self, let (messages, groupId, notify) = messageList.last, let firstMessage = messages.first {
if UIApplication.shared.applicationState == .active {
var chatIsVisible = false
if let topController = strongSelf.rootController.topViewController as? ChatController, topController.traceVisibility() {
if case .peer(firstMessage.id.peerId) = topController.chatLocation {
chatIsVisible = true
} else if case let .group(topGroupId) = topController.chatLocation, topGroupId == groupId {
chatIsVisible = true
}
}
if !notify {
chatIsVisible = true
}
if !chatIsVisible {
strongSelf.mainWindow.forEachViewController({ controller in
if let controller = controller as? ChatController, case .peer(firstMessage.id.peerId) = controller.chatLocation {
chatIsVisible = true
return false
}
return true
})
}
let inAppNotificationSettings: InAppNotificationSettings
if let current = strongSelf.inAppNotificationSettings {
inAppNotificationSettings = current
} else {
inAppNotificationSettings = InAppNotificationSettings.defaultSettings
}
if !strongSelf.isLocked {
if inAppNotificationSettings.playSounds {
serviceSoundManager.playIncomingMessageSound()
}
if inAppNotificationSettings.vibrate {
serviceSoundManager.playVibrationSound()
}
}
if chatIsVisible {
return
}
if inAppNotificationSettings.displayPreviews {
let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 }
strongSelf.notificationController.enqueue(ChatMessageNotificationItem(account: strongSelf.account, strings: presentationData.strings, messages: messages, tapAction: {
if let strongSelf = self {
var foundOverlay = false
strongSelf.mainWindow.forEachViewController({ controller in
if isOverlayControllerForChatNotificationOverlayPresentation(controller) {
foundOverlay = true
return false
}
return true
})
if foundOverlay {
return true
}
if let topController = strongSelf.rootController.topViewController as? ViewController, isInlineControllerForChatNotificationOverlayPresentation(topController) {
return true
}
if let topController = strongSelf.rootController.topViewController as? ChatController, case .peer(firstMessage.id.peerId) = topController.chatLocation {
strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId)
return false
}
for controller in strongSelf.rootController.viewControllers {
if let controller = controller as? ChatController, case .peer(firstMessage.id.peerId) = controller.chatLocation {
return true
}
}
strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId)
navigateToChatController(navigationController: strongSelf.rootController, account: strongSelf.account, chatLocation: .peer(firstMessage.id.peerId))
}
return false
}, expandAction: { expandData in
if let strongSelf = self {
let chatController = ChatController(account: strongSelf.account, chatLocation: .peer(firstMessage.id.peerId), mode: .overlay)
(strongSelf.rootController.viewControllers.last as? ViewController)?.present(chatController, in: .window(.root), with: ChatControllerOverlayPresentationData(expandData: expandData()))
}
}))
}
}
}
}))
self.termsOfServiceUpdatesDisposable.set((account.stateManager.termsOfServiceUpdate
|> deliverOnMainQueue).start(next: { [weak self] termsOfServiceUpdate in
guard let strongSelf = self else {
return
}
if strongSelf.currentTermsOfServiceUpdate == termsOfServiceUpdate {
return
}
strongSelf.currentTermsOfServiceUpdate = termsOfServiceUpdate
strongSelf.currentTermsOfServiceUpdateController?.dismiss()
strongSelf.currentTermsOfServiceUpdateController = nil
if let termsOfServiceUpdate = termsOfServiceUpdate {
let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 }
var acceptImpl: ((String?) -> Void)?
var declineImpl: (() -> Void)?
let controller = TermsOfServiceController(theme: TermsOfServiceControllerTheme(presentationTheme: presentationData.theme), strings: presentationData.strings, text: termsOfServiceUpdate.text, entities: termsOfServiceUpdate.entities, ageConfirmation: termsOfServiceUpdate.ageConfirmation, signingUp: false, accept: { proccedBot in
acceptImpl?(proccedBot)
}, decline: {
declineImpl?()
}, openUrl: { url in
if let parsedUrl = URL(string: url) {
UIApplication.shared.openURL(parsedUrl)
}
})
acceptImpl = { [weak controller] botName in
controller?.inProgress = true
guard let strongSelf = self else {
return
}
let _ = (acceptTermsOfService(account: strongSelf.account, id: termsOfServiceUpdate.id)
|> deliverOnMainQueue).start(completed: {
controller?.dismiss()
if let botName = botName {
self?.proccedTOSBotDisposable.set((resolvePeerByName(account: account, name: botName, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
self?.rootController.pushViewController(ChatController(account: account, chatLocation: .peer(peerId), messageId: nil))
}
}))
}
})
}
declineImpl = {
guard let strongSelf = self else {
return
}
let _ = (strongSelf.account.postbox.loadedPeerWithId(strongSelf.account.peerId)
|> deliverOnMainQueue).start(next: { peer in
if let phone = (peer as? TelegramUser)?.phone {
UIApplication.shared.openURL(URL(string: "https://telegram.org/deactivate?phone=\(phone)")!)
}
})
}
(strongSelf.rootController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root))
}
}))
self.displayAlertsDisposable = (account.stateManager.displayAlerts |> deliverOnMainQueue).start(next: { [weak self] alerts in
if let strongSelf = self{
for text in alerts {
let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 }
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
(strongSelf.rootController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root))
}
}
})
self.removeNotificationsDisposable = (account.stateManager.appliedIncomingReadMessages
|> deliverOnMainQueue).start(next: { [weak self] ids in
if let strongSelf = self {
strongSelf.applicationContext.applicationBindings.clearMessageNotifications(ids)
}
})
self.callDisposable = (callManager.currentCallSignal
|> deliverOnMainQueue).start(next: { [weak self] call in
if let strongSelf = self {
if call !== strongSelf.callController?.call {
strongSelf.callController?.dismiss()
strongSelf.callController = nil
strongSelf.hasOngoingCall.set(false)
if let call = call {
let callController = CallController(account: account, call: call)
strongSelf.callController = callController
strongSelf.rootController.view?.endEditing(true)
strongSelf.mainWindow.present(callController, on: .calls)
strongSelf.callState.set(call.state
|> map(Optional.init))
strongSelf.hasOngoingCall.set(true)
strongSelf.notificationManager.notificationCall = call
} else {
strongSelf.callState.set(.single(nil))
strongSelf.hasOngoingCall.set(false)
strongSelf.notificationManager.notificationCall = nil
}
}
}
})
self.callStateDisposable = (self.callState.get()
|> deliverOnMainQueue).start(next: { [weak self] state in
if let strongSelf = self {
let resolvedText: CallStatusText
if let state = state {
switch state {
case .connecting, .requesting, .terminating, .ringing, .waiting:
resolvedText = .inProgress(nil)
case .terminated:
resolvedText = .none
case let .active(timestamp, _):
resolvedText = .inProgress(timestamp)
}
} else {
resolvedText = .none
}
if strongSelf.currentCallStatusText != resolvedText {
strongSelf.currentCallStatusText = resolvedText
var referenceTimestamp: Double?
if case let .inProgress(timestamp) = resolvedText, let concreteTimestamp = timestamp {
referenceTimestamp = concreteTimestamp
}
if let _ = referenceTimestamp {
if strongSelf.currentCallStatusTextTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: {
if let strongSelf = self {
strongSelf.updateStatusBarText()
}
}, queue: Queue.mainQueue())
strongSelf.currentCallStatusTextTimer = timer
timer.start()
}
} else {
strongSelf.currentCallStatusTextTimer?.invalidate()
strongSelf.currentCallStatusTextTimer = nil
}
strongSelf.updateStatusBarText()
}
}
})
self.account.resetStateManagement()
let contactSynchronizationPreferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.contactSynchronizationSettings]))
let importableContacts = self.applicationContext.contactDataManager.importable()
self.account.importableContacts.set(self.account.postbox.combinedView(keys: [contactSynchronizationPreferencesKey])
|> mapToSignal { preferences -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError> in
let settings: ContactSynchronizationSettings = ((preferences.views[contactSynchronizationPreferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings) ?? .defaultSettings
if settings.synchronizeDeviceContacts {
return importableContacts
} else {
return .single([:])
}
})
let previousTheme = Atomic<PresentationTheme?>(value: nil)
self.presentationDataDisposable = (applicationContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
if previousTheme.swap(presentationData.theme) !== presentationData.theme {
strongSelf.mainWindow.previewThemeAccentColor = presentationData.theme.rootController.navigationBar.accentTextColor
strongSelf.mainWindow.previewThemeDarkBlur = presentationData.theme.chatList.searchBarKeyboardColor == .dark
strongSelf.lockedCoveringView.updateTheme(presentationData.theme)
strongSelf.rootController.updateTheme(NavigationControllerTheme(presentationTheme: presentationData.theme))
}
}
})
let showCallsTabSignal = account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.callListSettings])
|> map { view -> Bool in
var value = true
if let settings = view.values[ApplicationSpecificPreferencesKeys.callListSettings] as? CallListSettings {
value = settings.showTab
}
return value
}
self.showCallsTabDisposable = (showCallsTabSignal |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
if strongSelf.showCallsTab != value {
strongSelf.showCallsTab = value
strongSelf.rootController.updateRootControllers(showCallsTab: value)
}
}
})
let _ = (watchManagerArguments |> deliverOnMainQueue).start(next: { [weak self] arguments in
guard let strongSelf = self else {
return
}
let watchManager = WatchManager(arguments: arguments)
strongSelf.applicationContext.watchManager = watchManager
runningWatchTasksPromise.set(watchManager.runningTasks)
strongSelf.watchNavigateToMessageDisposable.set((strongSelf.applicationContext.applicationBindings.applicationInForeground |> mapToSignal({ applicationInForeground -> Signal<(Bool, MessageId), NoError> in
return watchManager.navigateToMessageRequested
|> map { messageId in
return (applicationInForeground, messageId)
}
|> deliverOnMainQueue
})).start(next: { [weak self] applicationInForeground, messageId in
if let strongSelf = self {
if applicationInForeground {
var chatIsVisible = false
if let controller = strongSelf.rootController.viewControllers.last as? ChatController, case .peer(messageId.peerId) = controller.chatLocation {
chatIsVisible = true
}
let navigateToMessage = {
navigateToChatController(navigationController: strongSelf.rootController, account: strongSelf.account, chatLocation: .peer(messageId.peerId), messageId: messageId)
}
if chatIsVisible {
navigateToMessage()
} else {
let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 }
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.WatchRemote_AlertTitle, text: presentationData.strings.WatchRemote_AlertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.WatchRemote_AlertOpen, action:navigateToMessage)])
(strongSelf.rootController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root))
}
} else {
strongSelf.notificationManager.presentWatchContinuityNotification(messageId: messageId)
}
}
}))
})
}
private func updateStatusBarText() {
if case let .inProgress(timestamp) = self.currentCallStatusText {
let text: String
let presentationData = self.applicationContext.currentPresentationData.with { $0 }
if let timestamp = timestamp {
let duration = Int32(CFAbsoluteTimeGetCurrent() - timestamp)
let durationString: String
if duration > 60 * 60 {
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
} else {
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
}
text = presentationData.strings.Call_StatusBar(durationString).0
} else {
text = presentationData.strings.Call_StatusBar("").0
}
self.mainWindow.setForceInCallStatusBar(text)
} else {
self.mainWindow.setForceInCallStatusBar(nil)
}
}
deinit {
self.account.postbox.clearCaches()
self.account.shouldKeepOnlinePresence.set(.single(false))
self.account.shouldBeServiceTaskMaster.set(.single(.never))
self.loggedOutDisposable.dispose()
self.inAppNotificationSettingsDisposable.dispose()
self.notificationMessagesDisposable.dispose()
self.termsOfServiceUpdatesDisposable.dispose()
self.passcodeLockDisposable.dispose()
self.passcodeStatusDisposable.dispose()
self.displayAlertsDisposable?.dispose()
self.removeNotificationsDisposable?.dispose()
self.callDisposable?.dispose()
self.callStateDisposable?.dispose()
self.currentCallStatusTextTimer?.invalidate()
self.presentationDataDisposable?.dispose()
self.enablePostboxTransactionsDiposable?.dispose()
self.proccedTOSBotDisposable.dispose()
self.watchNavigateToMessageDisposable.dispose()
}
func openChatWithPeerId(peerId: PeerId, messageId: MessageId? = nil) {
var visiblePeerId: PeerId?
if let controller = self.rootController.topViewController as? ChatController, case let .peer(peerId) = controller.chatLocation {
visiblePeerId = peerId
}
if visiblePeerId != peerId || messageId != nil {
if self.rootController.rootTabController != nil {
navigateToChatController(navigationController: self.rootController, account: self.account, chatLocation: .peer(peerId), messageId: messageId)
} else {
self.scheduledOperChatWithPeerId = peerId
}
}
}
func openUrl(_ url: URL) {
if self.rootController.rootTabController != nil {
let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 }
openExternalUrl(account: self.account, url: url.absoluteString, presentationData: presentationData, applicationContext: self.applicationContext, navigationController: self.rootController, dismissInput: { [weak self] in
self?.rootController.view.endEditing(true)
})
} else {
self.scheduledOpenExternalUrl = url
}
}
func openRootSearch() {
self.rootController.openChatsSearch()
}
func openRootCompose() {
self.rootController.openRootCompose()
}
func openRootCamera() {
self.rootController.openRootCamera()
}
private func updateCoveringViewSnaphot(_ visible: Bool) {
if visible {
let scale: CGFloat = 0.5
let unscaledSize = self.mainWindow.hostView.containerView.frame.size
let image = generateImage(CGSize(width: floor(unscaledSize.width * scale), height: floor(unscaledSize.height * scale)), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: scale, y: scale)
UIGraphicsPushContext(context)
self.mainWindow.hostView.containerView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false)
UIGraphicsPopContext()
})?.applyScreenshotEffect()
self.lockedCoveringView.updateSnapshot(image)
} else {
self.lockedCoveringView.updateSnapshot(nil)
}
}
}