mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-30 15:10:56 +00:00
924 lines
46 KiB
Swift
924 lines
46 KiB
Swift
import Foundation
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import TelegramCallsUI
|
|
import TelegramUIPreferences
|
|
import AccountContext
|
|
|
|
private enum CallStatusText: Equatable {
|
|
case none
|
|
case inProgress(Double?)
|
|
}
|
|
|
|
private final class AccountUserInterfaceInUseContext {
|
|
let subscribers = Bag<(Bool) -> Void>()
|
|
let tokens = Bag<Void>()
|
|
|
|
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 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 SharedAccountContextImpl: 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<Data?, NoError>
|
|
private let voipNotificationToken: Signal<Data?, NoError>
|
|
|
|
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<AccountRecordId>()
|
|
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<UnauthorizedAccount?>()
|
|
public var activeUnauthorizedAccount: Signal<UnauthorizedAccount?, NoError> {
|
|
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<Bool>(false)
|
|
private let callState = Promise<PresentationCallState?>(nil)
|
|
|
|
private var immediateHasOngoingCallValue = Atomic<Bool>(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)
|
|
|
|
private let _currentPresentationData: Atomic<PresentationData>
|
|
public var currentPresentationData: Atomic<PresentationData> {
|
|
return self._currentPresentationData
|
|
}
|
|
private let _presentationData = Promise<PresentationData>()
|
|
public var presentationData: Signal<PresentationData, NoError> {
|
|
return self._presentationData.get()
|
|
}
|
|
private let presentationDataDisposable = MetaDisposable()
|
|
|
|
public let currentInAppNotificationSettings: Atomic<InAppNotificationSettings>
|
|
private var inAppNotificationSettingsDisposable: Disposable?
|
|
|
|
public let currentAutomaticMediaDownloadSettings: Atomic<MediaAutoDownloadSettings>
|
|
private let _automaticMediaDownloadSettings = Promise<MediaAutoDownloadSettings>()
|
|
public var automaticMediaDownloadSettings: Signal<MediaAutoDownloadSettings, NoError> {
|
|
return self._automaticMediaDownloadSettings.get()
|
|
}
|
|
|
|
public let currentMediaInputSettings: Atomic<MediaInputSettings>
|
|
private var mediaInputSettingsDisposable: Disposable?
|
|
|
|
private let automaticMediaDownloadSettingsDisposable = MetaDisposable()
|
|
|
|
private var immediateExperimentalUISettingsValue = Atomic<ExperimentalUISettings>(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<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, 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<CachedMediaResourceRepresentationResult, NoError> 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, applicationInForeground: self.applicationBindings.applicationInForeground)
|
|
))
|
|
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("SharedAccountContextImpl: 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<AddedAccountResult, NoError>] = []
|
|
var addedAuthSignal: Signal<UnauthorizedAccount?, NoError> = .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<UnauthorizedAccount?, NoError> 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("SharedAccountContextImpl: 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<AccountPeerKey>()
|
|
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<AccountWithInfo?, NoError> 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("SharedAccountContextImpl 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<Never, NoError> {
|
|
return accountBackupData(postbox: account.postbox)
|
|
|> mapToSignal { backupData -> Signal<Never, NoError> 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 settings = self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings])
|
|
|> map { sharedData -> (allAccounts: Bool, includeMuted: Bool) in
|
|
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings
|
|
return (settings.displayNotificationsFromAllAccounts, settings.totalUnreadCountDisplayStyle == .raw)
|
|
}
|
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
|
if lhs.allAccounts != rhs.allAccounts {
|
|
return false
|
|
}
|
|
if lhs.includeMuted != rhs.includeMuted {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
self.registeredNotificationTokensDisposable.set((combineLatest(queue: .mainQueue(), settings, self.activeAccounts)
|
|
|> mapToSignal { settings, activeAccountsAndInfo -> Signal<Never, NoError> in
|
|
let (primary, activeAccounts, _) = activeAccountsAndInfo
|
|
var applied: [Signal<Never, NoError>] = []
|
|
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 !settings.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<Never, NoError>
|
|
let appliedVoip: Signal<Never, NoError>
|
|
|
|
if !activeProductionUserIds.contains(account.peerId.id) && !activeTestingUserIds.contains(account.peerId.id) {
|
|
appliedAps = self.apsNotificationToken
|
|
|> distinctUntilChanged(isEqual: { $0 == $1 })
|
|
|> mapToSignal { token -> Signal<Never, NoError> 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<Never, NoError> 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<Never, NoError> 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 }), excludeMutedChats: !settings.includeMuted)
|
|
}
|
|
appliedVoip = self.voipNotificationToken
|
|
|> distinctUntilChanged(isEqual: { $0 == $1 })
|
|
|> mapToSignal { token -> Signal<Never, NoError> 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 }), excludeMutedChats: !settings.includeMuted)
|
|
}
|
|
}
|
|
|
|
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<Bool, NoError> {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func handleTextLinkAction(context: AccountContext, peerId: PeerId?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) {
|
|
handleTextLinkActionImpl(context: context as! AccountContextImpl, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
|
|
}
|
|
}
|