import Foundation import Postbox import TelegramCore import SwiftSignalKit import Display private enum CallStatusText: Equatable { case none case inProgress(Double?) } private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() let tokens = Bag() var isEmpty: Bool { return self.tokens.isEmpty && self.subscribers.isEmpty } } public final class AccountWithInfo: Equatable { public let account: Account public let peer: Peer init(account: Account, peer: Peer) { self.account = account self.peer = peer } public static func ==(lhs: AccountWithInfo, rhs: AccountWithInfo) -> Bool { if lhs.account !== rhs.account { return false } if !arePeersEqual(lhs.peer, rhs.peer) { return false } return true } } private func pathFromLegacyFile(basePath: String, fileId: Int64, isLocal: Bool, fileName: String) -> String { let documentsPath = basePath + "/Documents" let filePath = documentsPath + "/files/" + (isLocal ? "local" : "") + "\(String(fileId, radix: 16))/\(fileName)" return filePath } private func preFetchedLegacyResourcePath(basePath: String, resource: MediaResource, cache: LegacyCache) -> String? { if let resource = resource as? CloudDocumentMediaResource { let videoPath = "\(basePath)/Documents/video/remote\(String(resource.fileId, radix: 16)).mov" if FileManager.default.fileExists(atPath: videoPath) { return videoPath } let fileName = resource.fileName?.replacingOccurrences(of: "/", with: "_") ?? "file" return pathFromLegacyFile(basePath: basePath, fileId: resource.fileId, isLocal: false, fileName: fileName) } else if let resource = resource as? CloudFileMediaResource { return cache.path(forCachedData: "\(resource.datacenterId)_\(resource.volumeId)_\(resource.localId)_\(resource.secret)") } return nil } private struct AccountAttributes: Equatable { let sortIndex: Int32 let isTestingEnvironment: Bool let backupData: AccountBackupData? } private enum AddedAccountResult { case upgrading(Float) case ready(AccountRecordId, Account?, Int32) } private enum AddedAccountsResult { case upgrading(Float) case ready([(AccountRecordId, Account?, Int32)]) } private var testHasInstance = false public final class SharedAccountContext { let mainWindow: Window1? public let applicationBindings: TelegramApplicationBindings public let basePath: String public let accountManager: AccountManager private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void private let apsNotificationToken: Signal private let voipNotificationToken: Signal private var activeAccountsValue: (primary: Account?, accounts: [(AccountRecordId, Account, Int32)], currentAuth: UnauthorizedAccount?)? private let activeAccountsPromise = Promise<(primary: Account?, accounts: [(AccountRecordId, Account, Int32)], currentAuth: UnauthorizedAccount?)>() public var activeAccounts: Signal<(primary: Account?, accounts: [(AccountRecordId, Account, Int32)], currentAuth: UnauthorizedAccount?), NoError> { return self.activeAccountsPromise.get() } private let managedAccountDisposables = DisposableDict() private let activeAccountsWithInfoPromise = Promise<(primary: AccountRecordId?, accounts: [AccountWithInfo])>() public var activeAccountsWithInfo: Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError> { return self.activeAccountsWithInfoPromise.get() } private var activeUnauthorizedAccountValue: UnauthorizedAccount? private let activeUnauthorizedAccountPromise = Promise() public var activeUnauthorizedAccount: Signal { return self.activeUnauthorizedAccountPromise.get() } private let registeredNotificationTokensDisposable = MetaDisposable() public let mediaManager: MediaManager public let contactDataManager: DeviceContactDataManager? let locationManager: DeviceLocationManager? public var callManager: PresentationCallManager? private var callDisposable: Disposable? private var callStateDisposable: Disposable? private var currentCallStatusText: CallStatusText = .none private var currentCallStatusTextTimer: SwiftSignalKit.Timer? private var callController: CallController? public let hasOngoingCall = ValuePromise(false) private let callState = Promise(nil) private var immediateHasOngoingCallValue = Atomic(value: false) public var immediateHasOngoingCall: Bool { return self.immediateHasOngoingCallValue.with { $0 } } private var hasOngoingCallDisposable: Disposable? private var accountUserInterfaceInUseContexts: [AccountRecordId: AccountUserInterfaceInUseContext] = [:] var switchingData: (settingsController: (SettingsController & ViewController)?, chatListController: ChatListController?, chatListBadge: String?) = (nil, nil, nil) public let currentPresentationData: Atomic private let _presentationData = Promise() public var presentationData: Signal { return self._presentationData.get() } private let presentationDataDisposable = MetaDisposable() public let currentInAppNotificationSettings: Atomic private var inAppNotificationSettingsDisposable: Disposable? public let currentAutomaticMediaDownloadSettings: Atomic private let _automaticMediaDownloadSettings = Promise() public var automaticMediaDownloadSettings: Signal { return self._automaticMediaDownloadSettings.get() } public let currentMediaInputSettings: Atomic private var mediaInputSettingsDisposable: Disposable? private let automaticMediaDownloadSettingsDisposable = MetaDisposable() private var immediateExperimentalUISettingsValue = Atomic(value: ExperimentalUISettings.defaultSettings) public var immediateExperimentalUISettings: ExperimentalUISettings { return self.immediateExperimentalUISettingsValue.with { $0 } } private var experimentalUISettingsDisposable: Disposable? public var presentGlobalController: (ViewController, Any?) -> Void = { _, _ in } public var presentCrossfadeController: () -> Void = {} private let displayUpgradeProgress: (Float?) -> Void public init(mainWindow: Window1?, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, rootPath: String, legacyBasePath: String?, legacyCache: LegacyCache?, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) testHasInstance = true self.mainWindow = mainWindow self.applicationBindings = applicationBindings self.basePath = basePath self.accountManager = accountManager self.navigateToChatImpl = navigateToChat self.displayUpgradeProgress = displayUpgradeProgress self.accountManager.mediaBox.fetchCachedResourceRepresentation = { (resource, representation) -> Signal in return fetchCachedSharedResourceRepresentation(accountManager: accountManager, resource: resource, representation: representation) } self.apsNotificationToken = apsNotificationToken self.voipNotificationToken = voipNotificationToken self.mediaManager = MediaManager(accountManager: accountManager, inForeground: applicationBindings.applicationInForeground) if applicationBindings.isMainApp { self.locationManager = DeviceLocationManager(queue: Queue.mainQueue()) self.contactDataManager = DeviceContactDataManager() } else { self.locationManager = nil self.contactDataManager = nil } self.currentPresentationData = Atomic(value: initialPresentationDataAndSettings.presentationData) self.currentAutomaticMediaDownloadSettings = Atomic(value: initialPresentationDataAndSettings.automaticMediaDownloadSettings) self.currentMediaInputSettings = Atomic(value: initialPresentationDataAndSettings.mediaInputSettings) self.currentInAppNotificationSettings = Atomic(value: initialPresentationDataAndSettings.inAppNotificationSettings) self._presentationData.set(.single(initialPresentationDataAndSettings.presentationData) |> then( updatedPresentationData(accountManager: self.accountManager, applicationBindings: self.applicationBindings) )) self._automaticMediaDownloadSettings.set(.single(initialPresentationDataAndSettings.automaticMediaDownloadSettings) |> then(accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) |> map { sharedData in let autodownloadSettings: AutodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings let automaticDownloadSettings: MediaAutoDownloadSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings] as? MediaAutoDownloadSettings ?? .defaultSettings return automaticDownloadSettings.updatedWithAutodownloadSettings(autodownloadSettings) } )) self.presentationDataDisposable.set((self.presentationData |> deliverOnMainQueue).start(next: { [weak self] next in if let strongSelf = self { var stringsUpdated = false var themeUpdated = false var themeNameUpdated = false let _ = strongSelf.currentPresentationData.modify { current in if next.strings !== current.strings { stringsUpdated = true } if next.theme !== current.theme { themeUpdated = true } if next.theme.name != current.theme.name { themeNameUpdated = true } return next } if stringsUpdated { updateLegacyLocalization(strings: next.strings) } if themeUpdated { updateLegacyTheme() } if themeNameUpdated { strongSelf.presentCrossfadeController() } } })) self.inAppNotificationSettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) |> deliverOnMainQueue).start(next: { [weak self] sharedData in if let strongSelf = self { if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings { let _ = strongSelf.currentInAppNotificationSettings.swap(settings) } } }) self.mediaInputSettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.mediaInputSettings]) |> deliverOnMainQueue).start(next: { [weak self] sharedData in if let strongSelf = self { if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaInputSettings] as? MediaInputSettings { let _ = strongSelf.currentMediaInputSettings.swap(settings) } } }) let immediateExperimentalUISettingsValue = self.immediateExperimentalUISettingsValue let _ = immediateExperimentalUISettingsValue.swap(initialPresentationDataAndSettings.experimentalUISettings) self.experimentalUISettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> deliverOnMainQueue).start(next: { sharedData in if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings] as? ExperimentalUISettings { let _ = immediateExperimentalUISettingsValue.swap(settings) } }) let _ = self.contactDataManager?.personNameDisplayOrder().start(next: { order in let _ = updateContactSettingsInteractively(accountManager: accountManager, { settings in var settings = settings settings.nameDisplayOrder = order return settings }).start() }) self.automaticMediaDownloadSettingsDisposable.set(self._automaticMediaDownloadSettings.get().start(next: { [weak self] next in if let strongSelf = self { let _ = strongSelf.currentAutomaticMediaDownloadSettings.swap(next) } })) let startTime = CFAbsoluteTimeGetCurrent() let differenceDisposable = MetaDisposable() let _ = (accountManager.accountRecords() |> map { view -> (AccountRecordId?, [AccountRecordId: AccountAttributes], (AccountRecordId, Bool)?) in print("SharedAccountContext: records appeared in \(CFAbsoluteTimeGetCurrent() - startTime)") var result: [AccountRecordId: AccountAttributes] = [:] for record in view.records { let isLoggedOut = record.attributes.contains(where: { attribute in return attribute is LoggedOutAccountAttribute }) if isLoggedOut { continue } let isTestingEnvironment = record.attributes.contains(where: { attribute in if let attribute = attribute as? AccountEnvironmentAttribute, case .test = attribute.environment { return true } else { return false } }) var backupData: AccountBackupData? var sortIndex: Int32 = 0 for attribute in record.attributes { if let attribute = attribute as? AccountSortOrderAttribute { sortIndex = attribute.order } else if let attribute = attribute as? AccountBackupDataAttribute { backupData = attribute.data } } result[record.id] = AccountAttributes(sortIndex: sortIndex, isTestingEnvironment: isTestingEnvironment, backupData: backupData) } let authRecord: (AccountRecordId, Bool)? = view.currentAuthAccount.flatMap({ authAccount in let isTestingEnvironment = authAccount.attributes.contains(where: { attribute in if let attribute = attribute as? AccountEnvironmentAttribute, case .test = attribute.environment { return true } else { return false } }) return (authAccount.id, isTestingEnvironment) }) return (view.currentRecord?.id, result, authRecord) } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs.0 != rhs.0 { return false } if lhs.1 != rhs.1 { return false } if lhs.2?.0 != rhs.2?.0 { return false } if lhs.2?.1 != rhs.2?.1 { return false } return true }) |> deliverOnMainQueue).start(next: { primaryId, records, authRecord in var addedSignals: [Signal] = [] var addedAuthSignal: Signal = .single(nil) for (id, attributes) in records { if self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == id}) == nil { addedSignals.append(accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, rootPath: rootPath, beginWithTestingEnvironment: attributes.isTestingEnvironment, backupData: attributes.backupData, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { result -> AddedAccountResult in switch result { case let .authorized(account): setupAccount(account, fetchCachedResourceRepresentation: fetchCachedResourceRepresentation, transformOutgoingMessageMedia: transformOutgoingMessageMedia, preFetchedResourcePath: { resource in if let legacyBasePath = legacyBasePath, let legacyCache = legacyCache { return preFetchedLegacyResourcePath(basePath: legacyBasePath, resource: resource, cache: legacyCache) } else { return nil } }) return .ready(id, account, attributes.sortIndex) case let .upgrading(progress): return .upgrading(progress) default: return .ready(id, nil, attributes.sortIndex) } }) } } if let authRecord = authRecord, authRecord.0 != self.activeAccountsValue?.currentAuth?.id { addedAuthSignal = accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: authRecord.0, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, rootPath: rootPath, beginWithTestingEnvironment: authRecord.1, backupData: nil, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> mapToSignal { result -> Signal in switch result { case let .unauthorized(account): return .single(account) case .upgrading: return .complete() default: return .single(nil) } } } let mappedAddedAccounts = combineLatest(queue: .mainQueue(), addedSignals) |> map { results -> AddedAccountsResult in var readyAccounts: [(AccountRecordId, Account?, Int32)] = [] var totalProgress: Float = 0.0 var hasItemsWithProgress = false for result in results { switch result { case let .ready(id, account, sortIndex): readyAccounts.append((id, account, sortIndex)) totalProgress += 1.0 case let .upgrading(progress): hasItemsWithProgress = true totalProgress += progress } } if hasItemsWithProgress, !results.isEmpty { return .upgrading(totalProgress / Float(results.count)) } else { return .ready(readyAccounts) } } differenceDisposable.set((combineLatest(queue: .mainQueue(), mappedAddedAccounts, addedAuthSignal) |> deliverOnMainQueue).start(next: { mappedAddedAccounts, authAccount in print("SharedAccountContext: accounts processed in \(CFAbsoluteTimeGetCurrent() - startTime)") var addedAccounts: [(AccountRecordId, Account?, Int32)] = [] switch mappedAddedAccounts { case let .upgrading(progress): self.displayUpgradeProgress(progress) return case let .ready(value): addedAccounts = value } self.displayUpgradeProgress(nil) var hadUpdates = false if self.activeAccountsValue == nil { self.activeAccountsValue = (nil, [], nil) hadUpdates = true } struct AccountPeerKey: Hashable { let peerId: PeerId let isTestingEnvironment: Bool } var existingAccountPeerKeys = Set() for accountRecord in addedAccounts { if let account = accountRecord.1 { if existingAccountPeerKeys.contains(AccountPeerKey(peerId: account.peerId, isTestingEnvironment: account.testingEnvironment)) { let _ = accountManager.transaction({ transaction in transaction.updateRecord(accountRecord.0, { _ in return nil }) }).start() } else { existingAccountPeerKeys.insert(AccountPeerKey(peerId: account.peerId, isTestingEnvironment: account.testingEnvironment)) if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == account.id }) { self.activeAccountsValue?.accounts.remove(at: index) self.managedAccountDisposables.set(nil, forKey: account.id) assertionFailure() } self.activeAccountsValue!.accounts.append((account.id, account, accountRecord.2)) self.managedAccountDisposables.set(self.updateAccountBackupData(account: account).start(), forKey: account.id) account.resetStateManagement() hadUpdates = true } } else { let _ = accountManager.transaction({ transaction in transaction.updateRecord(accountRecord.0, { _ in return nil }) }).start() } } var removedIds: [AccountRecordId] = [] for id in self.activeAccountsValue!.accounts.map({ $0.0 }) { if records[id] == nil { removedIds.append(id) } } for id in removedIds { hadUpdates = true if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == id }) { self.activeAccountsValue?.accounts.remove(at: index) self.managedAccountDisposables.set(nil, forKey: id) } } var primary: Account? if let primaryId = primaryId { if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == primaryId }) { primary = self.activeAccountsValue?.accounts[index].1 } } if primary == nil && !self.activeAccountsValue!.accounts.isEmpty { primary = self.activeAccountsValue!.accounts.first?.1 } if primary !== self.activeAccountsValue!.primary { hadUpdates = true self.activeAccountsValue!.primary?.postbox.clearCaches() self.activeAccountsValue!.primary?.resetCachedData() self.activeAccountsValue!.primary = primary } if self.activeAccountsValue!.currentAuth?.id != authRecord?.0 { hadUpdates = true self.activeAccountsValue!.currentAuth?.postbox.clearCaches() self.activeAccountsValue!.currentAuth = nil } if let authAccount = authAccount { hadUpdates = true self.activeAccountsValue!.currentAuth = authAccount } if hadUpdates { self.activeAccountsValue!.accounts.sort(by: { $0.2 < $1.2 }) self.activeAccountsPromise.set(.single(self.activeAccountsValue!)) } if self.activeAccountsValue!.primary == nil && self.activeAccountsValue!.currentAuth == nil { self.beginNewAuth(testingEnvironment: false) } })) }) self.activeAccountsWithInfoPromise.set(self.activeAccounts |> mapToSignal { primary, accounts, _ -> Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError> in return combineLatest(accounts.map { _, account, _ -> Signal in let peerViewKey: PostboxViewKey = .peer(peerId: account.peerId, components: []) return account.postbox.combinedView(keys: [peerViewKey]) |> map { view -> AccountWithInfo? in guard let peerView = view.views[peerViewKey] as? PeerView, let peer = peerView.peers[peerView.peerId] else { return nil } return AccountWithInfo(account: account, peer: peer) } |> distinctUntilChanged }) |> map { accountsWithInfo -> (primary: AccountRecordId?, accounts: [AccountWithInfo]) in var accountsWithInfoResult: [AccountWithInfo] = [] for info in accountsWithInfo { if let info = info { accountsWithInfoResult.append(info) } } return (primary?.id, accountsWithInfoResult) } }) if let mainWindow = mainWindow, applicationBindings.isMainApp { let callManager = PresentationCallManager(accountManager: self.accountManager, getDeviceAccessData: { return (self.currentPresentationData.with { $0 }, { [weak self] c, a in self?.presentGlobalController(c, a) }, { applicationBindings.openSettings() }) }, audioSession: self.mediaManager.audioSession, activeAccounts: self.activeAccounts |> map { _, accounts, _ in return Array(accounts.map({ $0.1 })) }) self.callManager = callManager 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 { mainWindow.hostView.containerView.endEditing(true) let callController = CallController(sharedContext: strongSelf, account: call.account, call: call) strongSelf.callController = callController strongSelf.mainWindow?.present(callController, on: .calls) strongSelf.callState.set(call.state |> map(Optional.init)) strongSelf.hasOngoingCall.set(true) setNotificationCall(call) } else { strongSelf.callState.set(.single(nil)) strongSelf.hasOngoingCall.set(false) setNotificationCall(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() } } }) mainWindow.inCallNavigate = { [weak self] in if let strongSelf = self, let callController = strongSelf.callController { if callController.isNodeLoaded && callController.view.superview == nil { mainWindow.hostView.containerView.endEditing(true) mainWindow.present(callController, on: .calls) } } } } else { self.callManager = nil } let immediateHasOngoingCallValue = self.immediateHasOngoingCallValue self.hasOngoingCallDisposable = self.hasOngoingCall.get().start(next: { value in let _ = immediateHasOngoingCallValue.swap(value) }) let _ = managedCleanupAccounts(networkArguments: networkArguments, accountManager: self.accountManager, rootPath: rootPath, auxiliaryMethods: telegramAccountAuxiliaryMethods, encryptionParameters: encryptionParameters).start() self.updateNotificationTokensRegistration() } deinit { assertionFailure("SharedAccountContext is not supposed to be deallocated") self.registeredNotificationTokensDisposable.dispose() self.presentationDataDisposable.dispose() self.automaticMediaDownloadSettingsDisposable.dispose() self.inAppNotificationSettingsDisposable?.dispose() self.mediaInputSettingsDisposable?.dispose() self.callDisposable?.dispose() self.callStateDisposable?.dispose() self.currentCallStatusTextTimer?.invalidate() } private func updateAccountBackupData(account: Account) -> Signal { return accountBackupData(postbox: account.postbox) |> mapToSignal { backupData -> Signal in guard let backupData = backupData else { return .complete() } return self.accountManager.transaction { transaction -> Void in transaction.updateRecord(account.id, { record in guard let record = record else { return nil } var attributes = record.attributes.filter({ !($0 is AccountBackupDataAttribute) }) attributes.append(AccountBackupDataAttribute(data: backupData)) return AccountRecord(id: record.id, attributes: attributes, temporarySessionId: record.temporarySessionId) }) } |> ignoreValues } } public func updateNotificationTokensRegistration() { let sandbox: Bool #if DEBUG sandbox = true #else sandbox = false #endif let allAccounts = self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) |> map { sharedData -> Bool in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings return settings.displayNotificationsFromAllAccounts } |> distinctUntilChanged self.registeredNotificationTokensDisposable.set((combineLatest(queue: .mainQueue(), allAccounts, self.activeAccounts) |> mapToSignal { allAccounts, activeAccountsAndInfo -> Signal in let (primary, activeAccounts, _) = activeAccountsAndInfo var applied: [Signal] = [] var activeProductionUserIds = activeAccounts.map({ $0.1 }).filter({ !$0.testingEnvironment }).map({ $0.peerId.id }) var activeTestingUserIds = activeAccounts.map({ $0.1 }).filter({ $0.testingEnvironment }).map({ $0.peerId.id }) let allProductionUserIds = activeProductionUserIds let allTestingUserIds = activeTestingUserIds if !allAccounts { if let primary = primary { if !primary.testingEnvironment { activeProductionUserIds = [primary.peerId.id] activeTestingUserIds = [] } else { activeProductionUserIds = [] activeTestingUserIds = [primary.peerId.id] } } else { activeProductionUserIds = [] activeTestingUserIds = [] } } for (_, account, _) in activeAccounts { let appliedAps: Signal let appliedVoip: Signal if !activeProductionUserIds.contains(account.peerId.id) && !activeTestingUserIds.contains(account.peerId.id) { appliedAps = self.apsNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token -> Signal in guard let token = token else { return .complete() } return unregisterNotificationToken(account: account, token: token, type: .aps(encrypt: false), otherAccountUserIds: (account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.peerId.id })) } appliedVoip = self.voipNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token -> Signal in guard let token = token else { return .complete() } return unregisterNotificationToken(account: account, token: token, type: .voip, otherAccountUserIds: (account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.peerId.id })) } } else { appliedAps = self.apsNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token -> Signal in guard let token = token else { return .complete() } let encrypt: Bool if #available(iOS 10.0, *) { encrypt = true } else { encrypt = false } return registerNotificationToken(account: account, token: token, type: .aps(encrypt: encrypt), sandbox: sandbox, otherAccountUserIds: (account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.peerId.id })) } appliedVoip = self.voipNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token -> Signal in guard let token = token else { return .complete() } return registerNotificationToken(account: account, token: token, type: .voip, sandbox: sandbox, otherAccountUserIds: (account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.peerId.id })) } } applied.append(appliedAps) applied.append(appliedVoip) } return combineLatest(applied) |> ignoreValues }).start()) } public func beginNewAuth(testingEnvironment: Bool) { let _ = self.accountManager.transaction({ transaction -> Void in let _ = transaction.createAuth([AccountEnvironmentAttribute(environment: testingEnvironment ? .test : .production)]) }).start() } public func switchToAccount(id: AccountRecordId, fromSettingsController settingsController: (SettingsController & ViewController)? = nil, withChatListController chatListController: ChatListController? = nil) { if self.activeAccountsValue?.primary?.id == id { return } assert(Queue.mainQueue().isCurrent()) var chatsBadge: String? if let rootController = self.mainWindow?.viewController as? TelegramRootController { if let tabsController = rootController.viewControllers.first as? TabBarController { for controller in tabsController.controllers { if let controller = controller as? ChatListController { chatsBadge = controller.tabBarItem.badgeValue } } if let chatListController = chatListController { if let index = tabsController.controllers.firstIndex(where: { $0 is ChatListController }) { var controllers = tabsController.controllers controllers[index] = chatListController tabsController.setControllers(controllers, selectedIndex: index) } } } } self.switchingData = (settingsController, chatListController, chatsBadge) let _ = self.accountManager.transaction({ transaction -> Bool in if transaction.getCurrent()?.0 != id { transaction.setCurrentId(id) return true } else { return false } }).start(next: { value in if !value { self.switchingData = (nil, nil, nil) } }) } public func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) { self.navigateToChatImpl(accountId, peerId, messageId) } private func updateStatusBarText() { if case let .inProgress(timestamp) = self.currentCallStatusText { let text: String let presentationData = self.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) } } public func navigateToCurrentCall() { if let mainWindow = self.mainWindow, let callController = self.callController { if callController.isNodeLoaded && callController.view.superview == nil { mainWindow.hostView.containerView.endEditing(true) mainWindow.present(callController, on: .calls) } } } public func accountUserInterfaceInUse(_ id: AccountRecordId) -> Signal { return Signal { subscriber in let context: AccountUserInterfaceInUseContext if let current = self.accountUserInterfaceInUseContexts[id] { context = current } else { context = AccountUserInterfaceInUseContext() self.accountUserInterfaceInUseContexts[id] = context } subscriber.putNext(!context.tokens.isEmpty) let index = context.subscribers.add({ value in subscriber.putNext(value) }) return ActionDisposable { [weak context] in Queue.mainQueue().async { if let current = self.accountUserInterfaceInUseContexts[id], current === context { current.subscribers.remove(index) if current.isEmpty { self.accountUserInterfaceInUseContexts.removeValue(forKey: id) } } } } } |> runOn(Queue.mainQueue()) } public func setAccountUserInterfaceInUse(_ id: AccountRecordId) -> Disposable { assert(Queue.mainQueue().isCurrent()) let context: AccountUserInterfaceInUseContext if let current = self.accountUserInterfaceInUseContexts[id] { context = current } else { context = AccountUserInterfaceInUseContext() self.accountUserInterfaceInUseContexts[id] = context } let wasEmpty = context.tokens.isEmpty let index = context.tokens.add(Void()) if wasEmpty { for f in context.subscribers.copyItems() { f(true) } } return ActionDisposable { [weak context] in Queue.mainQueue().async { if let current = self.accountUserInterfaceInUseContexts[id], current === context { let wasEmpty = current.tokens.isEmpty current.tokens.remove(index) if current.tokens.isEmpty && !wasEmpty { for f in current.subscribers.copyItems() { f(false) } } if current.isEmpty { self.accountUserInterfaceInUseContexts.removeValue(forKey: id) } } } } } }