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 struct AccountAttributes: Equatable { let sortIndex: Int32 let isTestingEnvironment: Bool } public final class SharedAccountContext { let mainWindow: Window1? public let applicationBindings: TelegramApplicationBindings 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 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 = {} public init(mainWindow: Window1?, accountManager: AccountManager, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, rootPath: String, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void) { assert(Queue.mainQueue().isCurrent()) self.mainWindow = mainWindow self.applicationBindings = applicationBindings self.accountManager = accountManager self.navigateToChatImpl = navigateToChat 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( updatedAutomaticMediaDownloadSettings(accountManager: self.accountManager) )) 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 differenceDisposable = MetaDisposable() let _ = (accountManager.accountRecords() |> map { view -> (AccountRecordId?, [AccountRecordId: AccountAttributes], (AccountRecordId, Bool)?) in 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 sortIndex: Int32 = 0 for attribute in record.attributes { if let attribute = attribute as? AccountSortOrderAttribute { sortIndex = attribute.order } } result[record.id] = AccountAttributes(sortIndex: sortIndex, isTestingEnvironment: isTestingEnvironment) } 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<(AccountRecordId, Account?, Int32), NoError>] = [] 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, supplementary: false, rootPath: rootPath, beginWithTestingEnvironment: attributes.isTestingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { result -> (AccountRecordId, Account?, Int32) in switch result { case let .authorized(account): return (id, account, attributes.sortIndex) default: return (id, nil, attributes.sortIndex) } }) } } if let authRecord = authRecord, authRecord.0 != self.activeAccountsValue?.currentAuth?.id { addedAuthSignal = accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: authRecord.0, supplementary: false, rootPath: rootPath, beginWithTestingEnvironment: authRecord.1, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { result -> UnauthorizedAccount? in switch result { case let .unauthorized(account): return account default: return nil } } } differenceDisposable.set((combineLatest(combineLatest(addedSignals), addedAuthSignal) |> deliverOnMainQueue).start(next: { accounts, authAccount in var hadUpdates = false if self.activeAccountsValue == nil { self.activeAccountsValue = (nil, [], nil) hadUpdates = true } for accountRecord in accounts { if let account = accountRecord.1 { if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == account.id }) { self.activeAccountsValue?.accounts.remove(at: index) assertionFailure() } self.activeAccountsValue!.accounts.append((account.id, account, accountRecord.2)) 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) } } 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 = 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).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() } public func updateNotificationTokensRegistration() { let sandbox: Bool #if DEBUG sandbox = true #else sandbox = false #endif self.registeredNotificationTokensDisposable.set((self.activeAccounts |> mapToSignal { _, activeAccounts, _ -> Signal in var applied: [Signal] = [] let activeProductionUserIds = activeAccounts.map({ $0.1 }).filter({ !$0.testingEnvironment }).map({ $0.peerId.id }) let activeTestingUserIds = activeAccounts.map({ $0.1 }).filter({ $0.testingEnvironment }).map({ $0.peerId.id }) for (_, account, _) in activeAccounts { let 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 })) } let 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) } } } } } }