mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00

Refactor animated sticker playback Move playback work to background Cache animated stickers' first frame previews Introduce cache lifetime classes
916 lines
45 KiB
Swift
916 lines
45 KiB
Swift
import Foundation
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import TelegramCallsUI
|
|
import TelegramUIPreferences
|
|
|
|
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 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)
|
|
|
|
public let currentPresentationData: Atomic<PresentationData>
|
|
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("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<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("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<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("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<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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|