import Foundation import UIKit import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import Display import TelegramPresentationData import TelegramCallsUI import TelegramUIPreferences import AccountContext import DeviceLocationManager import LegacyUI import ChatListUI import PeersNearbyUI import PeerInfoUI import SettingsUI import UrlHandling import LegacyMediaPickerUI import LocalMediaResources import OverlayStatusController import AlertUI import PresentationDataUtils import LocationUI import AppLock import WallpaperBackgroundNode import InAppPurchaseManager import PremiumUI import StickerPackPreviewUI import ChatControllerInteraction import ChatPresentationInterfaceState import StorageUsageScreen import DebugSettingsUI import MediaPickerUI import Photos import TextFormat import ChatTextLinkEditUI import AttachmentTextInputPanelNode import ChatEntityKeyboardInputNode import HashtagSearchUI import PeerInfoStoryGridScreen import TelegramAccountAuxiliaryMethods import PeerSelectionController import LegacyMessageInputPanel import StatisticsUI import ChatHistoryEntry import ChatMessageItem import ChatMessageItemImpl import ChatRecentActionsController import PeerInfoScreen import ChatQrCodeScreen import UndoUI import ChatMessageNotificationItem import ChatbotSetupScreen import BusinessLocationSetupScreen import BusinessHoursSetupScreen import AutomaticBusinessMessageSetupScreen import CollectibleItemInfoScreen import StickerPickerScreen import MediaEditor import MediaEditorScreen import BusinessIntroSetupScreen import TelegramNotices import BotSettingsScreen import CameraScreen import BirthdayPickerScreen import StarsTransactionsScreen import StarsPurchaseScreen import StarsTransferScreen import StarsTransactionScreen import StarsWithdrawalScreen import MiniAppListScreen import GiftOptionsScreen import GiftViewScreen import StarsIntroScreen import ContentReportScreen import AffiliateProgramSetupScreen import GalleryUI import ShareController import AccountFreezeInfoScreen import JoinSubjectScreen import OldChannelsController import InviteLinksUI import GiftStoreScreen import SendInviteLinkScreen import PostSuggestionsSettingsScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() let tokens = Bag() var isEmpty: Bool { return self.tokens.isEmpty && self.subscribers.isEmpty } } typealias AccountInitialData = (limitsConfiguration: LimitsConfiguration?, contentSettings: ContentSettings?, appConfiguration: AppConfiguration?, availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions) private struct AccountAttributes: Equatable { let sortIndex: Int32 let isTestingEnvironment: Bool let backupData: AccountBackupData? let isSupportUser: Bool } private enum AddedAccountResult { case upgrading(Float) case ready(AccountRecordId, Account?, Int32, AccountInitialData) } private enum AddedAccountsResult { case upgrading(Float) case ready([(AccountRecordId, Account?, Int32, AccountInitialData)]) } private var testHasInstance = false public final class SharedAccountContextImpl: SharedAccountContext { public let mainWindow: Window1? public let applicationBindings: TelegramApplicationBindings public let sharedContainerPath: String public let basePath: String public let networkArguments: NetworkInitializationArguments public let accountManager: AccountManager public let appLockContext: AppLockContext public var notificationController: NotificationContainerController? { didSet { if self.notificationController !== oldValue { if let oldValue { oldValue.setBlocking(nil) } } } } private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?, Bool) -> Void private let apsNotificationToken: Signal private let voipNotificationToken: Signal public let firebaseSecretStream: Signal<[String: String], NoError> private let authorizationPushConfigurationValue = Promise(nil) public var authorizationPushConfiguration: Signal { return self.authorizationPushConfigurationValue.get() } private var activeAccountsValue: (primary: AccountContext?, accounts: [(AccountRecordId, AccountContext, Int32)], currentAuth: UnauthorizedAccount?)? private let activeAccountsPromise = Promise<(primary: AccountContext?, accounts: [(AccountRecordId, AccountContext, Int32)], currentAuth: UnauthorizedAccount?)>() public var activeAccountContexts: Signal<(primary: AccountContext?, accounts: [(AccountRecordId, AccountContext, Int32)], currentAuth: UnauthorizedAccount?), NoError> { return self.activeAccountsPromise.get() } private let managedAccountDisposables = DisposableDict() private let activeAccountsWithInfoPromise = Promise<(primary: AccountRecordId?, accounts: [AccountWithInfo])>() public var activeAccountsWithInfo: Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError> { return self.activeAccountsWithInfoPromise.get() } private var activeUnauthorizedAccountValue: UnauthorizedAccount? private let activeUnauthorizedAccountPromise = Promise() public var activeUnauthorizedAccount: Signal { return self.activeUnauthorizedAccountPromise.get() } private let registeredNotificationTokensDisposable = MetaDisposable() public let mediaManager: MediaManager public let contactDataManager: DeviceContactDataManager? public let locationManager: DeviceLocationManager? public var callManager: PresentationCallManager? let hasInAppPurchases: Bool private var callStateDisposable: Disposable? private(set) var currentCallStatusBarNode: CallStatusBarNodeImpl? private var callDisposable: Disposable? private var groupCallDisposable: Disposable? private var callController: CallController? private var currentCall: PresentationCurrentCall? public let hasOngoingCall = ValuePromise(false) private var awaitingCallConnectionDisposable: Disposable? private var callPeerDisposable: Disposable? private var callIsConferenceDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { return self.groupCallController } private var hasGroupCallOnScreenValue: Bool = false private let hasGroupCallOnScreenPromise = Promise(false) public var hasGroupCallOnScreen: Signal { return self.hasGroupCallOnScreenPromise.get() } private var streamController: MediaStreamComponentController? private var immediateHasOngoingCallValue = Atomic(value: false) public var immediateHasOngoingCall: Bool { return self.immediateHasOngoingCallValue.with { $0 } } private var hasOngoingCallDisposable: Disposable? public let enablePreloads = Promise() public let hasPreloadBlockingContent = Promise(false) public let deviceContactPhoneNumbers = Promise>(Set()) private var accountUserInterfaceInUseContexts: [AccountRecordId: AccountUserInterfaceInUseContext] = [:] var switchingData: (settingsController: (SettingsController & ViewController)?, chatListController: ChatListController?, chatListBadge: String?) = (nil, nil, nil) private let _currentPresentationData: Atomic public var currentPresentationData: Atomic { return self._currentPresentationData } 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 var currentAutomaticMediaDownloadSettings: MediaAutoDownloadSettings private let _automaticMediaDownloadSettings = Promise() public var automaticMediaDownloadSettings: Signal { return self._automaticMediaDownloadSettings.get() } public private(set) var energyUsageSettings: EnergyUsageSettings public let currentAutodownloadSettings: Atomic private let _autodownloadSettings = Promise() private var currentAutodownloadSettingsDisposable = MetaDisposable() public let currentMediaInputSettings: Atomic private var mediaInputSettingsDisposable: Disposable? public let currentMediaDisplaySettings: Atomic private var mediaDisplaySettingsDisposable: Disposable? public let currentStickerSettings: Atomic private var stickerSettingsDisposable: Disposable? private let automaticMediaDownloadSettingsDisposable = MetaDisposable() private var immediateExperimentalUISettingsValue = Atomic(value: ExperimentalUISettings.defaultSettings) public var immediateExperimentalUISettings: ExperimentalUISettings { return self.immediateExperimentalUISettingsValue.with { $0 } } private var experimentalUISettingsDisposable: Disposable? public var presentGlobalController: (ViewController, Any?) -> Void = { _, _ in } public var presentCrossfadeController: () -> Void = {} private let displayUpgradeProgress: (Float?) -> Void private var spotlightDataContext: SpotlightDataContext? private var widgetDataContext: WidgetDataContext? private weak var appDelegate: AppDelegate? private var invalidatedApsToken: Data? private let energyUsageAutomaticDisposable = MetaDisposable() init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) testHasInstance = true self.appDelegate = appDelegate self.mainWindow = mainWindow self.applicationBindings = applicationBindings self.sharedContainerPath = sharedContainerPath self.basePath = basePath self.networkArguments = networkArguments self.accountManager = accountManager self.navigateToChatImpl = navigateToChat self.displayUpgradeProgress = displayUpgradeProgress self.appLockContext = appLockContext self.notificationController = notificationController self.hasInAppPurchases = hasInAppPurchases self.accountManager.mediaBox.fetchCachedResourceRepresentation = { (resource, representation) -> Signal in return fetchCachedSharedResourceRepresentation(accountManager: accountManager, resource: resource, representation: representation) } self.apsNotificationToken = apsNotificationToken self.voipNotificationToken = voipNotificationToken self.firebaseSecretStream = firebaseSecretStream self.authorizationPushConfigurationValue.set(apsNotificationToken |> map { data -> AuthorizationCodePushNotificationConfiguration? in guard let data else { return nil } let sandbox: Bool #if DEBUG sandbox = true #else sandbox = false #endif return AuthorizationCodePushNotificationConfiguration( token: hexString(data), isSandbox: sandbox ) }) if applicationBindings.isMainApp { self.locationManager = DeviceLocationManager(queue: Queue.mainQueue()) self.contactDataManager = DeviceContactDataManagerImpl() } else { self.locationManager = nil self.contactDataManager = nil } self._currentPresentationData = Atomic(value: initialPresentationDataAndSettings.presentationData) self.currentAutomaticMediaDownloadSettings = initialPresentationDataAndSettings.automaticMediaDownloadSettings self.currentAutodownloadSettings = Atomic(value: initialPresentationDataAndSettings.autodownloadSettings) self.currentMediaInputSettings = Atomic(value: initialPresentationDataAndSettings.mediaInputSettings) self.currentMediaDisplaySettings = Atomic(value: initialPresentationDataAndSettings.mediaDisplaySettings) self.currentStickerSettings = Atomic(value: initialPresentationDataAndSettings.stickerSettings) self.currentInAppNotificationSettings = Atomic(value: initialPresentationDataAndSettings.inAppNotificationSettings) if automaticEnergyUsageShouldBeOnNow(settings: self.currentAutomaticMediaDownloadSettings) { self.energyUsageSettings = EnergyUsageSettings.powerSavingDefault } else { self.energyUsageSettings = self.currentAutomaticMediaDownloadSettings.energyUsageSettings } let presentationData: Signal = .single(initialPresentationDataAndSettings.presentationData) |> then( updatedPresentationData(accountManager: self.accountManager, applicationInForeground: self.applicationBindings.applicationInForeground, systemUserInterfaceStyle: mainWindow?.systemUserInterfaceStyle ?? .single(.light)) ) self._presentationData.set(presentationData) self._automaticMediaDownloadSettings.set(.single(initialPresentationDataAndSettings.automaticMediaDownloadSettings) |> then(accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) |> map { sharedData in let autodownloadSettings: AutodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings let automaticDownloadSettings: MediaAutoDownloadSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) ?? .defaultSettings return automaticDownloadSettings.updatedWithAutodownloadSettings(autodownloadSettings) } )) self.mediaManager = MediaManagerImpl(accountManager: accountManager, inForeground: applicationBindings.applicationInForeground, presentationData: presentationData) self.mediaManager.overlayMediaManager.updatePossibleEmbeddingItem = { [weak self] item in guard let strongSelf = self else { return } guard let navigationController = strongSelf.mainWindow?.viewController as? NavigationController else { return } var content: NavigationControllerDropContent? if let item = item { content = NavigationControllerDropContent( position: item.position, item: VideoNavigationControllerDropContentItem( itemNode: item.itemNode ) ) } navigationController.updatePossibleControllerDropContent(content: content) } self.mediaManager.overlayMediaManager.embedPossibleEmbeddingItem = { [weak self] item in guard let strongSelf = self else { return false } guard let navigationController = strongSelf.mainWindow?.viewController as? NavigationController else { return false } let content = NavigationControllerDropContent( position: item.position, item: VideoNavigationControllerDropContentItem( itemNode: item.itemNode ) ) return navigationController.acceptPossibleControllerDropContent(content: content) } self._autodownloadSettings.set(.single(initialPresentationDataAndSettings.autodownloadSettings) |> then(accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> map { sharedData in let autodownloadSettings: AutodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings return 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 #available(iOS 13.0, *) { let userInterfaceStyle: UIUserInterfaceStyle if strongSelf.currentPresentationData.with({ $0 }).theme.overallDarkAppearance { userInterfaceStyle = .dark } else { userInterfaceStyle = .light } if let eventView = strongSelf.mainWindow?.hostView.eventView, eventView.overrideUserInterfaceStyle != userInterfaceStyle { eventView.overrideUserInterfaceStyle = userInterfaceStyle } }*/ } 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]?.get(InAppNotificationSettings.self) { 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]?.get(MediaInputSettings.self) { let _ = strongSelf.currentMediaInputSettings.swap(settings) } } }) self.mediaDisplaySettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.mediaDisplaySettings]) |> deliverOnMainQueue).start(next: { [weak self] sharedData in if let strongSelf = self { if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) { let _ = strongSelf.currentMediaDisplaySettings.swap(settings) } } }) self.stickerSettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]) |> deliverOnMainQueue).start(next: { [weak self] sharedData in if let strongSelf = self { if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { let _ = strongSelf.currentStickerSettings.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]?.get(ExperimentalUISettings.self) { let _ = immediateExperimentalUISettingsValue.swap(settings) flatBuffers_checkedGet = settings.checkSerializedData } }) 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 { strongSelf.currentAutomaticMediaDownloadSettings = next if automaticEnergyUsageShouldBeOnNow(settings: next) { strongSelf.energyUsageSettings = EnergyUsageSettings.powerSavingDefault } else { strongSelf.energyUsageSettings = next.energyUsageSettings } strongSelf.energyUsageAutomaticDisposable.set((automaticEnergyUsageShouldBeOn(settings: next) |> deliverOnMainQueue).start(next: { value in if let strongSelf = self { if value { strongSelf.energyUsageSettings = EnergyUsageSettings.powerSavingDefault } else { strongSelf.energyUsageSettings = next.energyUsageSettings } } })) } })) self.currentAutodownloadSettingsDisposable.set(self._autodownloadSettings.get().start(next: { [weak self] next in if let strongSelf = self { let _ = strongSelf.currentAutodownloadSettings.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 if case .loggedOut = attribute { return true } else { return false } }) if isLoggedOut { continue } let isTestingEnvironment = record.attributes.contains(where: { attribute in if case let .environment(environment) = attribute, case .test = environment.environment { return true } else { return false } }) var backupData: AccountBackupData? var sortIndex: Int32 = 0 var isSupportUser = false for attribute in record.attributes { if case let .sortOrder(sortOrder) = attribute { sortIndex = sortOrder.order } else if case let .backupData(backupDataValue) = attribute { backupData = backupDataValue.data } else if case .supportUserInfo = attribute, !"".isEmpty { isSupportUser = true } } result[record.id] = AccountAttributes(sortIndex: sortIndex, isTestingEnvironment: isTestingEnvironment, backupData: backupData, isSupportUser: isSupportUser) } let authRecord: (AccountRecordId, Bool)? = view.currentAuthAccount.flatMap({ authAccount in let isTestingEnvironment = authAccount.attributes.contains(where: { attribute in if case let .environment(environment) = attribute, case .test = environment.environment { return true } else { return false } }) return (authAccount.id, isTestingEnvironment) }) return (view.currentRecord?.id, result, authRecord) } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs.0 != rhs.0 { return false } if lhs.1 != rhs.1 { return false } if lhs.2?.0 != rhs.2?.0 { return false } if lhs.2?.1 != rhs.2?.1 { return false } return true }) |> deliverOnMainQueue).start(next: { primaryId, records, authRecord in var addedSignals: [Signal] = [] var addedAuthSignal: Signal = .single(nil) for (id, attributes) in records { if self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == id}) == nil { addedSignals.append(accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, isSupportUser: attributes.isSupportUser, rootPath: rootPath, beginWithTestingEnvironment: attributes.isTestingEnvironment, backupData: attributes.backupData, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: appDelegate?.uploadInBackround)) |> mapToSignal { result -> Signal in switch result { case let .authorized(account): setupAccount(account, fetchCachedResourceRepresentation: fetchCachedResourceRepresentation, transformOutgoingMessageMedia: transformOutgoingMessageMedia) return TelegramEngine(account: account).data.get( TelegramEngine.EngineData.Item.Configuration.Limits(), TelegramEngine.EngineData.Item.Configuration.ContentSettings(), TelegramEngine.EngineData.Item.Configuration.App(), TelegramEngine.EngineData.Item.Configuration.AvailableColorOptions(scope: .replies), TelegramEngine.EngineData.Item.Configuration.AvailableColorOptions(scope: .profile) ) |> map { limitsConfiguration, contentSettings, appConfiguration, availableReplyColors, availableProfileColors -> AddedAccountResult in return .ready(id, account, attributes.sortIndex, (limitsConfiguration._asLimits(), contentSettings, appConfiguration, availableReplyColors, availableProfileColors)) } case let .upgrading(progress): return .single(.upgrading(progress)) default: return .single(.ready(id, nil, attributes.sortIndex, (nil, nil, nil, EngineAvailableColorOptions(hash: 0, options: []), EngineAvailableColorOptions(hash: 0, options: [])))) } }) } } if let authRecord = authRecord, authRecord.0 != self.activeAccountsValue?.currentAuth?.id { addedAuthSignal = accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: authRecord.0, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, isSupportUser: false, rootPath: rootPath, beginWithTestingEnvironment: authRecord.1, backupData: nil, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: appDelegate?.uploadInBackround)) |> mapToSignal { result -> Signal in switch result { case let .unauthorized(account): return .single(account) case .upgrading: return .complete() default: return .single(nil) } } } let mappedAddedAccounts = combineLatest(queue: .mainQueue(), addedSignals) |> map { results -> AddedAccountsResult in var readyAccounts: [(AccountRecordId, Account?, Int32, AccountInitialData)] = [] var totalProgress: Float = 0.0 var hasItemsWithProgress = false for result in results { switch result { case let .ready(id, account, sortIndex, initialData): readyAccounts.append((id, account, sortIndex, initialData)) 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, AccountInitialData)] = [] switch mappedAddedAccounts { case let .upgrading(progress): self.displayUpgradeProgress(progress) return case let .ready(value): addedAccounts = value } self.displayUpgradeProgress(nil) var hadUpdates = false if self.activeAccountsValue == nil { self.activeAccountsValue = (nil, [], nil) hadUpdates = true } struct AccountPeerKey: Hashable { let peerId: PeerId let isTestingEnvironment: Bool } var existingAccountPeerKeys = Set() for accountRecord in addedAccounts { if let account = accountRecord.1 { if existingAccountPeerKeys.contains(AccountPeerKey(peerId: account.peerId, isTestingEnvironment: account.testingEnvironment)) { let _ = accountManager.transaction({ transaction in transaction.updateRecord(accountRecord.0, { _ in return nil }) }).start() } else { existingAccountPeerKeys.insert(AccountPeerKey(peerId: account.peerId, isTestingEnvironment: account.testingEnvironment)) if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == account.id }) { self.activeAccountsValue?.accounts.remove(at: index) self.managedAccountDisposables.set(nil, forKey: account.id) assertionFailure() } let context = AccountContextImpl(sharedContext: self, account: account, limitsConfiguration: accountRecord.3.limitsConfiguration ?? .defaultValue, contentSettings: accountRecord.3.contentSettings ?? .default, appConfiguration: accountRecord.3.appConfiguration ?? .defaultValue, availableReplyColors: accountRecord.3.availableReplyColors, availableProfileColors: accountRecord.3.availableProfileColors) self.activeAccountsValue!.accounts.append((account.id, context, 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: AccountContext? 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?.account.postbox.clearCaches() self.activeAccountsValue!.primary?.account.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!)) self.performAccountSettingsImportIfNecessary() } if self.activeAccountsValue!.primary == nil && self.activeAccountsValue!.currentAuth == nil { self.beginNewAuth(testingEnvironment: false) } })) }) self.activeAccountsWithInfoPromise.set(self.activeAccountContexts |> mapToSignal { primary, accounts, _ -> Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError> in return combineLatest(accounts.map { _, context, _ -> Signal in return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> AccountWithInfo? in guard let peer = peer else { return nil } return AccountWithInfo(account: context.account, peer: peer._asPeer()) } |> distinctUntilChanged }) |> map { accountsWithInfo -> (primary: AccountRecordId?, accounts: [AccountWithInfo]) in var accountsWithInfoResult: [AccountWithInfo] = [] for info in accountsWithInfo { if let info = info { accountsWithInfoResult.append(info) } } return (primary?.account.id, accountsWithInfoResult) } }) if let mainWindow = mainWindow, applicationBindings.isMainApp { let callManager = PresentationCallManagerImpl(accountManager: self.accountManager, getDeviceAccessData: { return (self.currentPresentationData.with { $0 }, { [weak self] c, a in self?.presentGlobalController(c, a) }, { applicationBindings.openSettings() }) }, isMediaPlaying: { [weak self] in guard let strongSelf = self else { return false } var result = false let _ = (strongSelf.mediaManager.globalMediaPlayerState |> take(1) |> deliverOnMainQueue).start(next: { state in if let (_, playbackState, _) = state, case let .state(value) = playbackState, case .playing = value.status.status { result = true } }) return result }, resumeMediaPlayback: { [weak self] in guard let strongSelf = self else { return } strongSelf.mediaManager.playlistControl(.playback(.play), type: nil) }, audioSession: self.mediaManager.audioSession, activeAccounts: self.activeAccountContexts |> map { _, accounts, _ in return Array(accounts.map({ $0.1 })) }) self.callManager = callManager self.callDisposable = (callManager.currentCallSignal |> deliverOnMainQueue).start(next: { [weak self] call in guard let self else { return } if let call { self.updateCurrentCall(call: .call(call)) } else if let current = self.currentCall, case .call = current { self.updateCurrentCall(call: nil) } /*if call !== self.call { let previousCall = self.call self.call = call self.callController?.dismiss() self.callController = nil self.hasOngoingCall.set(false) self.callState.set(.single(nil)) if let previousCall, let groupCallController = self.groupCallController { var matches = false switch groupCallController.call { case let .conferenceSource(conferenceSource): if conferenceSource === previousCall { matches = true } case let .group(groupCall): if (groupCall as? PresentationGroupCallImpl)?.upgradedConferenceCall === previousCall { matches = true } } if matches { self.groupCallController = nil groupCallController.dismiss(closing: true, manual: false) } } self.notificationController?.setBlocking(nil) self.callPeerDisposable?.dispose() self.callPeerDisposable = nil self.callIsConferenceDisposable?.dispose() self.callIsConferenceDisposable = nil if let call { self.hasOngoingCall.set(true) setNotificationCall(call) self.callIsConferenceDisposable = (call.conferenceState |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] _ in guard let self else { return } guard let call = self.call else { return } guard let callController = self.callController, callController.call === call else { if self.callController == nil, call.conferenceStateValue != nil { self.presentControllerWithCurrentCall() self.notificationController?.setBlocking(nil) } return } if call.conferenceStateValue != nil { self.presentControllerWithCurrentCall() } }) if call.isOutgoing { self.presentControllerWithCurrentCall() } else { if !call.isIntegratedWithCallKit { self.callPeerDisposable?.dispose() self.callPeerDisposable = (call.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) |> deliverOnMainQueue).startStrict(next: { [weak self, weak call] peer in guard let self, let call, let peer else { return } if self.call !== call { return } let presentationData = self.currentPresentationData.with { $0 } self.notificationController?.setBlocking(ChatCallNotificationItem(context: call.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, peer: peer, isVideo: call.isVideo, action: { [weak call] answerAction in guard let call else { return } if answerAction { call.answer() } else { call.rejectBusy() } })) }) } self.awaitingCallConnectionDisposable = (call.state |> filter { state in switch state.state { case .ringing: return false case .terminating, .terminated: return false default: return true } } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } self.notificationController?.setBlocking(nil) self.presentControllerWithCurrentCall() self.callPeerDisposable?.dispose() self.callPeerDisposable = nil }) } } else { self.callState.set(.single(nil)) self.hasOngoingCall.set(false) self.awaitingCallConnectionDisposable?.dispose() self.awaitingCallConnectionDisposable = nil setNotificationCall(nil) } }*/ }) self.groupCallDisposable = (callManager.currentGroupCallSignal |> deliverOnMainQueue).start(next: { [weak self] call in guard let self else { return } if let call { self.updateCurrentCall(call: .group(call)) } else if let current = self.currentCall, case .group = current { self.updateCurrentCall(call: nil) } }) mainWindow.inCallNavigate = { [weak self] in guard let self else { return } if let callController = self.callController { mainWindow.hostView.containerView.endEditing(true) if callController.view.superview == nil { if useFlatModalCallsPresentation(context: callController.call.context) { (mainWindow.viewController as? NavigationController)?.pushViewController(callController) } else { mainWindow.present(callController, on: .calls) } } else { callController.expandFromPipIfPossible() } } else if let groupCallController = self.groupCallController { mainWindow.hostView.containerView.endEditing(true) if groupCallController.view.superview == nil { (mainWindow.viewController as? NavigationController)?.pushViewController(groupCallController) } } else if let streamController = self.streamController { mainWindow.hostView.containerView.endEditing(true) if streamController.view.superview == nil { (mainWindow.viewController as? NavigationController)?.pushViewController(streamController) } } } } else { self.callManager = nil } let immediateHasOngoingCallValue = self.immediateHasOngoingCallValue self.hasOngoingCallDisposable = self.hasOngoingCall.get().start(next: { value in let _ = immediateHasOngoingCallValue.swap(value) }) self.enablePreloads.set(combineLatest( self.hasOngoingCall.get(), self.hasPreloadBlockingContent.get() ) |> map { hasOngoingCall, hasPreloadBlockingContent -> Bool in if hasOngoingCall { return false } if hasPreloadBlockingContent { return false } return true }) let _ = managedCleanupAccounts(networkArguments: networkArguments, accountManager: self.accountManager, rootPath: rootPath, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: appDelegate?.uploadInBackround), encryptionParameters: encryptionParameters).start() self.updateNotificationTokensRegistration() if applicationBindings.isMainApp { self.widgetDataContext = WidgetDataContext(basePath: self.basePath, inForeground: self.applicationBindings.applicationInForeground, activeAccounts: self.activeAccountContexts |> map { _, accounts, _ in return accounts.map { $0.1.account } }, presentationData: self.presentationData, appLockContext: self.appLockContext as! AppLockContextImpl) let enableSpotlight = accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.intentsSettings])) |> map { sharedData -> Bool in let intentsSettings: IntentsSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.intentsSettings]?.get(IntentsSettings.self) ?? .defaultSettings return intentsSettings.contacts } |> distinctUntilChanged self.spotlightDataContext = SpotlightDataContext(appBasePath: applicationBindings.containerPath, accountManager: accountManager, accounts: combineLatest(enableSpotlight, self.activeAccountContexts |> map { _, accounts, _ in return accounts.map { _, account, _ in return account.account } }) |> map { enableSpotlight, accounts in if enableSpotlight { return accounts } else { return [] } }) } /*if #available(iOS 13.0, *) { let userInterfaceStyle: UIUserInterfaceStyle if self.currentPresentationData.with({ $0 }).theme.overallDarkAppearance { userInterfaceStyle = .dark } else { userInterfaceStyle = .light } if let eventView = self.mainWindow?.hostView.eventView, eventView.overrideUserInterfaceStyle != userInterfaceStyle { eventView.overrideUserInterfaceStyle = userInterfaceStyle } }*/ } deinit { assertionFailure("SharedAccountContextImpl is not supposed to be deallocated") self.registeredNotificationTokensDisposable.dispose() self.presentationDataDisposable.dispose() self.automaticMediaDownloadSettingsDisposable.dispose() self.currentAutodownloadSettingsDisposable.dispose() self.inAppNotificationSettingsDisposable?.dispose() self.mediaInputSettingsDisposable?.dispose() self.mediaDisplaySettingsDisposable?.dispose() self.callDisposable?.dispose() self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() self.awaitingCallConnectionDisposable?.dispose() self.callPeerDisposable?.dispose() } private var didPerformAccountSettingsImport = false private func performAccountSettingsImportIfNecessary() { if self.didPerformAccountSettingsImport { return } if let _ = UserDefaults.standard.value(forKey: "didPerformAccountSettingsImport") { self.didPerformAccountSettingsImport = true return } UserDefaults.standard.set(true as NSNumber, forKey: "didPerformAccountSettingsImport") UserDefaults.standard.synchronize() if let primary = self.activeAccountsValue?.primary { let _ = (primary.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: primary.account.peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, case let .user(user) = peer else { return } if user.isPremium { let _ = updateMediaDownloadSettingsInteractively(accountManager: self.accountManager, { settings in var settings = settings settings.energyUsageSettings.loopEmoji = true return settings }).start() } }) } self.didPerformAccountSettingsImport = true } private func updateAccountBackupData(account: Account) -> Signal { return accountBackupData(postbox: account.postbox) |> mapToSignal { backupData -> Signal in guard let backupData = backupData else { return .complete() } return self.accountManager.transaction { transaction -> Void in transaction.updateRecord(account.id, { record in guard let record = record else { return nil } var attributes: [TelegramAccountManagerTypes.Attribute] = record.attributes.filter { attribute in if case .backupData = attribute { return false } else { return true } } attributes.append(.backupData(AccountBackupDataAttribute(data: backupData))) return AccountRecord(id: record.id, attributes: attributes, temporarySessionId: record.temporarySessionId) }) } |> ignoreValues } } private func updateCurrentCall(call: PresentationCurrentCall?) { if self.currentCall == call { return } if let currentCall = self.currentCall { if case .call = currentCall { self.callPeerDisposable?.dispose() self.callPeerDisposable = nil self.awaitingCallConnectionDisposable?.dispose() self.awaitingCallConnectionDisposable = nil self.notificationController?.setBlocking(nil) } } self.currentCall = call let beginDisplayingCallStatusBar = Promise() var shouldResetGroupCallOnScreen = true var transitioningToConferenceCallController: CallController? if let call, case let .group(groupCall) = call, case let .conferenceSource(conferenceSource) = groupCall, let callController = self.callController, callController.call === conferenceSource { transitioningToConferenceCallController = callController if callController.navigationPresentation != .flatModal { callController.dismissWithoutAnimation() } self.callController = nil shouldResetGroupCallOnScreen = false } if let callController = self.callController { self.callController = nil callController.dismiss() } if let groupCallController = self.groupCallController { if case let .group(groupCall) = call, case let .group(groupCall) = groupCall, let conferenceSourceId = groupCall.conferenceSource { if case let .conferenceSource(conferenceSource) = groupCallController.call, conferenceSource.internalId == conferenceSourceId { groupCallController.updateCall(call: .group(groupCall)) self.updateInCallStatusBarData(hasGroupCallOnScreen: self.hasGroupCallOnScreenValue) return } } self.groupCallController = nil groupCallController.dismiss() } if let streamController = self.streamController { self.streamController = nil streamController.dismiss() } if shouldResetGroupCallOnScreen { self.hasGroupCallOnScreenPromise.set(.single(false)) } self.callStateDisposable?.dispose() self.callStateDisposable = nil if case let .call(call) = call { let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) self.callController = callController let thisCallIsOnScreenPromise = ValuePromise(false, ignoreRepeated: true) callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in guard let self, let callController else { completion(false) return } if callController.window == nil { if useFlatModalCallsPresentation(context: callController.call.context) { (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) } else { self.mainWindow?.present(callController, on: .calls) } } completion(true) } callController.onViewDidAppear = { thisCallIsOnScreenPromise.set(true) } callController.onViewDidDisappear = { thisCallIsOnScreenPromise.set(false) } if call.isOutgoing { self.mainWindow?.hostView.containerView.endEditing(true) thisCallIsOnScreenPromise.set(true) self.hasGroupCallOnScreenPromise.set(thisCallIsOnScreenPromise.get()) if useFlatModalCallsPresentation(context: callController.call.context) { (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) } else { self.mainWindow?.present(callController, on: .calls) } } else { self.hasGroupCallOnScreenPromise.set(thisCallIsOnScreenPromise.get()) if !call.isIntegratedWithCallKit { self.callPeerDisposable = (call.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) |> deliverOnMainQueue).startStrict(next: { [weak self, weak call] peer in guard let self, let call, let peer else { return } if self.currentCall != .call(call) { return } let presentationData = self.currentPresentationData.with({ $0 }) self.notificationController?.setBlocking(ChatCallNotificationItem( context: call.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, peer: peer, isVideo: call.isVideo, action: { [weak call] answerAction in guard let call else { return } if answerAction { self.notificationController?.setBlocking(nil) call.answer() } else { self.notificationController?.setBlocking(nil) call.rejectBusy() } } )) }) } self.awaitingCallConnectionDisposable = (call.state |> filter { state in switch state.state { case .ringing: return false case .terminating, .terminated: return false default: return true } } |> take(1) |> deliverOnMainQueue).start(next: { [weak self, weak callController] _ in guard let self, let callController, self.callController === callController else { return } self.notificationController?.setBlocking(nil) self.callPeerDisposable?.dispose() self.callPeerDisposable = nil thisCallIsOnScreenPromise.set(true) if useFlatModalCallsPresentation(context: callController.call.context) { (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) } else { self.mainWindow?.present(callController, on: .calls) } }) } beginDisplayingCallStatusBar.set(call.state |> filter { state in switch state.state { case .ringing: return false case .terminating, .terminated: return false default: return true } } |> take(1) |> map { _ -> Void in return Void() }) } var groupCallIsStream = false if case let .group(groupCall) = call, case let .group(value) = groupCall { groupCallIsStream = value.isStream } if case let .group(groupCall) = call, !groupCallIsStream { let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: groupCall.accountContext, call: groupCall) |> deliverOnMainQueue).start(next: { [weak self, weak transitioningToConferenceCallController] initialData in guard let self else { return } guard let navigationController = self.mainWindow?.viewController as? NavigationController else { return } let thisCallIsOnScreenPromise = ValuePromise(false, ignoreRepeated: true) let groupCallController = makeVoiceChatController(sharedContext: self, accountContext: groupCall.accountContext, call: groupCall, initialData: initialData, sourceCallController: transitioningToConferenceCallController) groupCallController.onViewDidAppear = { thisCallIsOnScreenPromise.set(true) } groupCallController.onViewDidDisappear = { thisCallIsOnScreenPromise.set(false) } groupCallController.navigationPresentation = .flatModal groupCallController.parentNavigationController = navigationController self.groupCallController = groupCallController self.mainWindow?.hostView.containerView.endEditing(true) thisCallIsOnScreenPromise.set(true) self.hasGroupCallOnScreenPromise.set(thisCallIsOnScreenPromise.get()) beginDisplayingCallStatusBar.set(.single(Void())) if let transitioningToConferenceCallController { transitioningToConferenceCallController.onViewDidAppear = nil transitioningToConferenceCallController.onViewDidDisappear = nil } if let transitioningToConferenceCallController { var viewControllers = navigationController.viewControllers if let index = viewControllers.firstIndex(where: { $0 === transitioningToConferenceCallController }) { viewControllers.insert(groupCallController, at: index) navigationController.setViewControllers(viewControllers, animated: false) #if DEBUG assert(viewControllers[index + 1] === transitioningToConferenceCallController) #endif viewControllers.removeAll(where: { $0 === transitioningToConferenceCallController }) navigationController.setViewControllers(viewControllers, animated: false) } else { navigationController.pushViewController(groupCallController) } } else { navigationController.pushViewController(groupCallController) } }) } if case let .group(groupCall) = call, case let .group(group) = groupCall, groupCallIsStream { if let navigationController = self.mainWindow?.viewController as? NavigationController { let streamController = MediaStreamComponentController(call: group) streamController.navigationPresentation = .flatModal streamController.parentNavigationController = navigationController let thisCallIsOnScreenPromise = ValuePromise(false, ignoreRepeated: true) streamController.onViewDidAppear = { thisCallIsOnScreenPromise.set(true) } streamController.onViewDidDisappear = { thisCallIsOnScreenPromise.set(false) } self.streamController = streamController self.mainWindow?.hostView.containerView.endEditing(true) thisCallIsOnScreenPromise.set(true) self.hasGroupCallOnScreenPromise.set(thisCallIsOnScreenPromise.get()) beginDisplayingCallStatusBar.set(.single(Void())) navigationController.pushViewController(streamController) } } if self.currentCall != nil { self.callStateDisposable = (combineLatest(queue: .mainQueue(), self.hasGroupCallOnScreenPromise.get(), beginDisplayingCallStatusBar.get() ) |> deliverOnMainQueue).startStrict(next: { [weak self] hasGroupCallOnScreen, _ in guard let self else { return } self.hasGroupCallOnScreenValue = hasGroupCallOnScreen self.updateInCallStatusBarData(hasGroupCallOnScreen: hasGroupCallOnScreen) }) } else { self.hasGroupCallOnScreenValue = false self.currentCallStatusBarNode = nil if let navigationController = self.mainWindow?.viewController as? NavigationController { navigationController.setForceInCallStatusBar(nil) } } } private func updateInCallStatusBarData(hasGroupCallOnScreen: Bool) { var statusBarContent: CallStatusBarNodeImpl.Content? if !hasGroupCallOnScreen, let currentCall = self.currentCall { switch currentCall { case let .call(call): statusBarContent = .call(self, call.context.account, call) case let .group(groupCall): switch groupCall { case let .conferenceSource(conferenceSource): statusBarContent = .call(self, conferenceSource.context.account, conferenceSource) case let .group(groupCall): statusBarContent = .groupCall(self, groupCall.account, groupCall) } } } var resolvedCallStatusBarNode: CallStatusBarNodeImpl? if let statusBarContent { if let current = self.currentCallStatusBarNode { resolvedCallStatusBarNode = current } else { resolvedCallStatusBarNode = CallStatusBarNodeImpl() self.currentCallStatusBarNode = resolvedCallStatusBarNode } resolvedCallStatusBarNode?.update(content: statusBarContent) } else { self.currentCallStatusBarNode = nil } if let navigationController = self.mainWindow?.viewController as? NavigationController { navigationController.setForceInCallStatusBar(resolvedCallStatusBarNode) } } private func presentControllerWithCurrentCall() { /*guard let call = self.call else { return } if call.conferenceStateValue != nil { if let groupCallController = self.groupCallController { if groupCallController.call == call.conferenceCall.flatMap(VideoChatCall.group) || groupCallController.call == .conferenceSource(call) { return } groupCallController.dismiss(closing: true, manual: false) self.groupCallController = nil } var transitioniongCallController: CallController? if let callController = self.callController { transitioniongCallController = callController if callController.navigationPresentation != .flatModal { callController.dismissWithoutAnimation() } self.callController = nil } let groupCall: VideoChatCall if let conferenceCall = call.conferenceCall, case .ready = call.conferenceStateValue { groupCall = .group(conferenceCall) } else { groupCall = .conferenceSource(call) } let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: call.context, call: groupCall) |> deliverOnMainQueue).start(next: { [weak self, weak transitioniongCallController] initialData in guard let self else { return } guard let navigationController = self.mainWindow?.viewController as? NavigationController else { return } guard let call = self.call else { return } let groupCall: VideoChatCall if let conferenceCall = call.conferenceCall, case .ready = call.conferenceStateValue { groupCall = .group(conferenceCall) } else { groupCall = .conferenceSource(call) } let groupCallController = makeVoiceChatController(sharedContext: self, accountContext: call.context, call: groupCall, initialData: initialData, sourceCallController: transitioniongCallController) groupCallController.onViewDidAppear = { [weak self] in if let self { self.hasGroupCallOnScreenPromise.set(true) } } groupCallController.onViewDidDisappear = { [weak self] in if let self { self.hasGroupCallOnScreenPromise.set(false) } } groupCallController.navigationPresentation = .flatModal groupCallController.parentNavigationController = navigationController self.groupCallController = groupCallController transitioniongCallController?.onViewDidAppear = nil transitioniongCallController?.onViewDidDisappear = nil self.hasGroupCallOnScreenPromise.set(true) if let transitioniongCallController, let navigationController = transitioniongCallController.navigationController as? NavigationController { var viewControllers = navigationController.viewControllers if let index = viewControllers.firstIndex(where: { $0 === transitioniongCallController }) { viewControllers.insert(groupCallController, at: index) navigationController.setViewControllers(viewControllers, animated: false) viewControllers.remove(at: index + 1) navigationController.setViewControllers(viewControllers, animated: false) } else { navigationController.pushViewController(groupCallController) } } else { navigationController.pushViewController(groupCallController) } }) } else { if let currentCallController = self.callController { if currentCallController.call === call { self.navigateToCurrentCall() return } else { self.callController = nil currentCallController.dismiss() } } self.mainWindow?.hostView.containerView.endEditing(true) let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) self.callController = callController callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in guard let self, let callController else { completion(false) return } if callController.window == nil { if useFlatModalCallsPresentation(context: callController.call.context) { (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) } else { self.mainWindow?.present(callController, on: .calls) } } completion(true) } callController.onViewDidAppear = { [weak self] in if let self { self.hasGroupCallOnScreenPromise.set(true) } } callController.onViewDidDisappear = { [weak self] in if let self { self.hasGroupCallOnScreenPromise.set(false) } } if useFlatModalCallsPresentation(context: callController.call.context) { self.hasGroupCallOnScreenPromise.set(true) (self.mainWindow?.viewController as? NavigationController)?.pushViewController(callController) } else { self.mainWindow?.present(callController, on: .calls) } }*/ } 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]?.get(InAppNotificationSettings.self) ?? InAppNotificationSettings.defaultSettings return (settings.displayNotificationsFromAllAccounts, false) } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs.allAccounts != rhs.allAccounts { return false } if lhs.includeMuted != rhs.includeMuted { return false } return true }) let updatedApsToken = self.apsNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) self.registeredNotificationTokensDisposable.set((combineLatest( queue: .mainQueue(), settings, self.activeAccountContexts, updatedApsToken ) |> mapToSignal { settings, activeAccountsAndInfo, apsNotificationToken -> Signal<(Bool, Data?), NoError> in let (primary, activeAccounts, _) = activeAccountsAndInfo var appliedApsList: [Signal] = [] var appliedVoipList: [Signal] = [] var activeProductionUserIds = activeAccounts.map({ $0.1 }).filter({ !$0.account.testingEnvironment }).map({ $0.account.peerId.id }) var activeTestingUserIds = activeAccounts.map({ $0.1 }).filter({ $0.account.testingEnvironment }).map({ $0.account.peerId.id }) let allProductionUserIds = activeProductionUserIds let allTestingUserIds = activeTestingUserIds if !settings.allAccounts { if let primary = primary { if !primary.account.testingEnvironment { activeProductionUserIds = [primary.account.peerId.id] activeTestingUserIds = [] } else { activeProductionUserIds = [] activeTestingUserIds = [primary.account.peerId.id] } } else { activeProductionUserIds = [] activeTestingUserIds = [] } } for (_, account, _) in activeAccounts { let appliedAps: Signal let appliedVoip: Signal if !activeProductionUserIds.contains(account.account.peerId.id) && !activeTestingUserIds.contains(account.account.peerId.id) { if let apsNotificationToken { appliedAps = account.engine.accountData.unregisterNotificationToken(token: apsNotificationToken, type: .aps(encrypt: false), otherAccountUserIds: (account.account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.account.peerId.id })) |> map { _ -> Bool in } |> then(.single(true)) } else { appliedAps = .single(true) } appliedVoip = self.voipNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token -> Signal in guard let token = token else { return .complete() } return account.engine.accountData.unregisterNotificationToken(token: token, type: .voip, otherAccountUserIds: (account.account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.account.peerId.id })) } } else { if let apsNotificationToken { appliedAps = account.engine.accountData.registerNotificationToken(token: apsNotificationToken, type: .aps(encrypt: true), sandbox: sandbox, otherAccountUserIds: (account.account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.account.peerId.id }), excludeMutedChats: !settings.includeMuted) } else { appliedAps = .single(true) } appliedVoip = self.voipNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { token -> Signal in guard let token = token else { return .complete() } return account.engine.accountData.registerNotificationToken(token: token, type: .voip, sandbox: sandbox, otherAccountUserIds: (account.account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.account.peerId.id }), excludeMutedChats: !settings.includeMuted) |> ignoreValues } } appliedApsList.append(Signal.single(nil) |> then(appliedAps |> map(Optional.init))) appliedVoipList.append(appliedVoip) } let allApsSuccess = combineLatest(appliedApsList) |> map { values -> Bool in return !values.contains(false) } let allVoipSuccess = combineLatest(appliedVoipList) return combineLatest( allApsSuccess, Signal.single(Void()) |> then( allVoipSuccess |> map { _ -> Void in return Void() } ) ) |> map { allApsSuccess, _ -> (Bool, Data?) in return (allApsSuccess, apsNotificationToken) } } |> deliverOnMainQueue).start(next: { [weak self] allApsSuccess, apsToken in guard let self, let appDelegate = self.appDelegate else { return } if !allApsSuccess { if self.invalidatedApsToken != apsToken { self.invalidatedApsToken = apsToken appDelegate.requestNotificationTokenInvalidation() } } })) } public func beginNewAuth(testingEnvironment: Bool) { let _ = self.accountManager.transaction({ transaction -> Void in let _ = transaction.createAuth([.environment(AccountEnvironmentAttribute(environment: testingEnvironment ? .test : .production))]) }).start() } public func switchToAccount(id: AccountRecordId, fromSettingsController settingsController: ViewController? = nil, withChatListController chatListController: ViewController? = nil) { if self.activeAccountsValue?.primary?.account.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 as? (ViewController & SettingsController), chatListController as? 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 openSearch(filter: ChatListSearchFilter, query: String?) { if let rootController = self.mainWindow?.viewController as? TelegramRootController { rootController.openChatsController(activateSearch: true, filter: filter, query: query) } } public func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) { self.navigateToChatImpl(accountId, peerId, messageId, true) } public func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> { let historyView = preloadedChatHistoryViewForLocation(location, context: context, chatLocation: chatLocation, subject: subject, chatLocationContextHolder: chatLocationContextHolder, fixedCombinedReadStates: nil, tag: tag, additionalData: []) return historyView |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in switch historyView { case .Loading: return .single((nil, true)) case let .HistoryView(view, _, _, _, _, _, _): for entry in view.entries { if entry.message.id == id { return .single((entry.message.index, false)) } } return .single((nil, false)) } } |> take(until: { index in return SignalTakeAction(passthrough: true, complete: !index.1) }) } public func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController { return OverlayAudioPlayerControllerImpl(context: context, chatLocation: chatLocation, type: type, initialMessageId: initialMessageId, initialOrder: initialOrder, playlistLocation: playlistLocation, parentNavigationController: parentNavigationController) } public func makeTempAccountContext(account: Account) -> AccountContext { return AccountContextImpl(sharedContext: self, account: account, limitsConfiguration: .defaultValue, contentSettings: .default, appConfiguration: .defaultValue, availableReplyColors: EngineAvailableColorOptions(hash: 0, options: []), availableProfileColors: EngineAvailableColorOptions(hash: 0, options: []), temp: true) } public func openChatMessage(_ params: OpenChatMessageParams) -> Bool { return openChatMessageImpl(params) } public func navigateToCurrentCall() { guard let mainWindow = self.mainWindow else { return } if let callController = self.callController { if callController.isNodeLoaded && callController.view.superview == nil { mainWindow.hostView.containerView.endEditing(true) if useFlatModalCallsPresentation(context: callController.call.context) { (mainWindow.viewController as? NavigationController)?.pushViewController(callController) } else { mainWindow.present(callController, on: .calls) } } } else if let groupCallController = self.groupCallController { if groupCallController.isNodeLoaded && groupCallController.view.superview == nil { mainWindow.hostView.containerView.endEditing(true) (mainWindow.viewController as? NavigationController)?.pushViewController(groupCallController) } } else if let streamController = self.streamController { if streamController.isNodeLoaded && streamController.view.superview == nil { mainWindow.hostView.containerView.endEditing(true) (mainWindow.viewController as? NavigationController)?.pushViewController(streamController) } } } 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) } } } } } public func handleTextLinkAction(context: AccountContext, peerId: PeerId?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) { handleTextLinkActionImpl(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) } public func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? { let controller = peerInfoControllerImpl(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: fromChat) controller?.navigationPresentation = .modalInLargeLayout return controller } public func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController? { let controller = channelAdminController(context: context, peerId: peerId, adminId: adminId, initialParticipant: initialParticipant, updated: { _ in }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in }) return controller } public func makeDebugSettingsController(context: AccountContext?) -> ViewController? { let controller = debugController(sharedContext: self, context: context) return controller } public func openCreateGroupCallUI(context: AccountContext, peerIds: [EnginePeer.Id], parentController: ViewController) { let _ = (context.engine.data.get( EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ) |> deliverOnMainQueue).startStandalone(next: { [weak parentController] peers in guard let parentController else { return } let peers = peers.compactMap({ $0 }) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams( context: context, title: presentationData.strings.Calls_NewCall, mode: .groupCreation(isCall: true), options: .single([]), filters: [.excludeSelf], onlyWriteable: true, isGroupInvitation: false, isPeerEnabled: nil, attemptDisabledItemSelection: nil, alwaysEnabled: false, limit: nil, reachedLimit: nil, openProfile: nil, sendMessage: nil, initialSelectedPeers: peers )) controller.navigationPresentation = .modal if let navigationController = parentController.navigationController as? NavigationController { navigationController.pushViewController(controller) } else if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { navigationController.pushViewController(controller) } let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak parentController] result in guard let parentController else { return } guard case let .result(rawPeerIds, _) = result else { controller?.dismiss() return } let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in if case let .peer(id) = id { return id } return nil } if peerIds.isEmpty { controller?.dismiss() return } let isVideo = controller?.isCallVideoOptionSelected ?? false if peerIds.count == 1 { controller?.dismiss() self.performCall(context: context, parentController: parentController, peerId: peerIds[0], isVideo: isVideo, began: { let _ = (context.sharedContext.hasOngoingCall.get() |> filter { $0 } |> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true)) |> delay(0.5, queue: Queue.mainQueue()) |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in if let controller, let navigationController = controller.navigationController as? NavigationController { if navigationController.viewControllers.last === controller { let _ = navigationController.popViewController(animated: true) } } }) }) } else { self.createGroupCall(context: context, parentController: parentController, peerIds: peerIds, isVideo: isVideo, completion: { controller?.dismiss() }) } }) }) } private func performCall(context: AccountContext, parentController: ViewController, peerId: EnginePeer.Id, isVideo: Bool, began: (() -> Void)? = nil) { let _ = (context.account.viewTracker.peerView(peerId) |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak parentController] view in guard let parentController else { return } guard let peer = peerViewMainPeer(view) else { return } if let cachedUserData = view.cachedData as? CachedUserData, cachedUserData.callsPrivate { let presentationData = context.sharedContext.currentPresentationData.with { $0 } parentController.present(textAlertController(context: context, title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } context.requestCall(peerId: peerId, isVideo: isVideo, completion: { began?() }) }) } private func createGroupCall(context: AccountContext, parentController: ViewController, peerIds: [EnginePeer.Id], isVideo: Bool, completion: (() -> Void)? = nil) { parentController.view.endEditing(true) var cancelImpl: (() -> Void)? var signal = context.engine.calls.createConferenceCall() let presentationData = context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { [weak parentController] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.3, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() signal = signal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } let disposable = (signal |> deliverOnMainQueue).startStandalone(next: { [weak parentController] call in guard let parentController else { return } let openCall: () -> Void = { let _ = context.sharedContext.callManager?.joinConferenceCall( accountContext: context, initialCall: EngineGroupCallDescription( id: call.callInfo.id, accessHash: call.callInfo.accessHash, title: call.callInfo.title, scheduleTimestamp: nil, subscribedToScheduled: false, isStream: false ), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), beginWithVideo: isVideo, invitePeerIds: peerIds, endCurrentIfAny: true ) completion?() } if !peerIds.isEmpty { openCall() } else { let controller = InviteLinkInviteController( context: context, updatedPresentationData: nil, mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)), initialInvite: .link(link: call.link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil), parentNavigationController: parentController.navigationController as? NavigationController, completed: { [weak parentController] result in guard let parentController else { return } if let result { switch result { case .linkCopied: let presentationData = context.sharedContext.currentPresentationData.with { $0 } parentController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: presentationData.strings.CallList_ToastCallLinkCopied_Text, customUndoText: presentationData.strings.CallList_ToastCallLinkCopied_Action, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in if case .undo = action { openCall() } return false }), in: .window(.root)) case .openCall: openCall() } } } ) parentController.present(controller, in: .window(.root), with: nil) } }) cancelImpl = { disposable.dispose() } } public func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) } public func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set, keepUpdated: Bool) -> Signal { return chatAvailableMessageActionsImpl(engine: engine, accountPeerId: accountPeerId, messageIds: messageIds, keepUpdated: keepUpdated) } public func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set, messages: [EngineMessage.Id: EngineMessage] = [:], peers: [EnginePeer.Id: EnginePeer] = [:]) -> Signal { return chatAvailableMessageActionsImpl(engine: engine, accountPeerId: accountPeerId, messageIds: messageIds, messages: messages.mapValues({ $0._asMessage() }), peers: peers.mapValues({ $0._asPeer() }), keepUpdated: false) } public func navigateToChatController(_ params: NavigateToChatControllerParams) { navigateToChatControllerImpl(params) } public func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) { navigateToForumChannelImpl(context: context, peerId: peerId, navigationController: navigationController) } public func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, scrollToEndIfExists: Bool, keepStack: NavigateToChatKeepStack) -> Signal { return navigateToForumThreadImpl(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: activateInput, scrollToEndIfExists: scrollToEndIfExists, keepStack: keepStack) } public func chatControllerForForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64) -> Signal { return chatControllerForForumThreadImpl(context: context, peerId: peerId, threadId: threadId) } public func openStorageUsage(context: AccountContext) { guard let navigationController = self.mainWindow?.viewController as? NavigationController else { return } let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in return storageUsageExceptionsScreen(context: context, category: category) }) navigationController.pushViewController(controller) } public func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController) { var found = false for controller in navigationController.viewControllers.reversed() { if let controller = controller as? LocationViewController, controller.subject.id.peerId == messageId.peerId { controller.goToUserLocation(visibleRadius: nil) found = true break } } if !found { let controllerParams = LocationViewParams(sendLiveLocation: { location in //let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil) // params.enqueueMessage(outMessage) }, stopLiveLocation: { messageId in if let messageId = messageId { context.liveLocationManager?.cancelLiveLocation(peerId: messageId.peerId) } }, openUrl: { _ in }, openPeer: { peer in // params.openPeer(peer, .info) }) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) |> deliverOnMainQueue).start(next: { message in guard let message = message else { return } let controller = LocationViewController(context: context, subject: message, params: controllerParams) controller.navigationPresentation = .modal navigationController.pushViewController(controller) }) } } public func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) |> mapToSignal { result -> Signal in switch result { case .progress: return .complete() case let .result(value): return .single(value) } } } public func resolveUrlWithProgress(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) } public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) { openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, forceUpdate: forceUpdate, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, sendEmoji: sendEmoji, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext, progress: progress, completion: completion) } public func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController { return deviceContactInfoController(context: context, environment: environment, subject: subject, completed: completed, cancelled: cancelled) } public func makePeersNearbyController(context: AccountContext) -> ViewController { return peersNearbyController(context: context) } public func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode, params: ChatControllerParams?) -> ChatController { return ChatControllerImpl(context: context, chatLocation: chatLocation, subject: subject, botStart: botStart, mode: mode, params: params) } public func makeChatHistoryListNode( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteractionProtocol, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode ) -> ChatHistoryListNode { return ChatHistoryListNodeImpl( context: context, updatedPresentationData: updatedPresentationData, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: tag, source: source, subject: subject, controllerInteraction: controllerInteraction as! ChatControllerInteraction, selectedMessages: selectedMessages, mode: mode, isChatPreview: false, messageTransitionNode: { return nil } ) } public func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? { return nil } public func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController { return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId, starsState: starsState) } public func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) { presentContactsWarningSuppressionImpl(context: context, present: present) } public func makeContactSelectionController(_ params: ContactSelectionControllerParams) -> ContactSelectionController { return ContactSelectionControllerImpl(params) } public func makeContactMultiselectionController(_ params: ContactMultiselectionControllerParams) -> ContactMultiselectionController { return ContactMultiselectionControllerImpl(params) } public func makeComposeController(context: AccountContext) -> ViewController { return ComposeControllerImpl(context: context) } public func makeProxySettingsController(context: AccountContext) -> ViewController { return proxySettingsController(context: context) } public func makeLocalizationListController(context: AccountContext) -> ViewController { return LocalizationListController(context: context) } public func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) { openAddContactImpl(context: context, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, label: label, present: present, pushController: pushController, completed: completed) } public func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) { openAddPersonContactImpl(context: context, peerId: peerId, pushController: pushController, present: present) } public func makeCreateGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String?, mode: CreateGroupMode, completion: ((PeerId, @escaping () -> Void) -> Void)?) -> ViewController { return createGroupControllerImpl(context: context, peerIds: peerIds, initialTitle: initialTitle, mode: mode, completion: completion) } public func makeChatListController(context: AccountContext, location: ChatListControllerLocation, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool, previewing: Bool, enableDebugActions: Bool) -> ChatListController { return ChatListControllerImpl(context: context, location: location, controlsHistoryPreload: controlsHistoryPreload, hideNetworkActivityStatus: hideNetworkActivityStatus, previewing: previewing, enableDebugActions: enableDebugActions) } public func makePeerSelectionController(_ params: PeerSelectionControllerParams) -> PeerSelectionController { return PeerSelectionControllerImpl(params) } public func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) { return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) } public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _, _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _, _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { message in tapMessage?(message) }, clickThroughMessage: { view, location in clickThroughMessage?(view, location) }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { return nil }, chatControllerNode: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, openConferenceCall: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return .none }, canSendMessages: { return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in }, rateCall: { _, _, _ in }, requestSelectMessagePollOptions: { _, _ in }, requestOpenMessagePollResults: { _, _ in }, openAppStorePage: { }, displayMessageTooltip: { _, _, _, _, _ in }, seekToTimecode: { _, _, _ in }, scheduleCurrentMessage: { _ in }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in }, displayDiceTooltip: { _ in }, animateDiceSuccess: { _, _ in }, displayPremiumStickerTooltip: { _, _ in }, displayEmojiPackTooltip: { _, _ in }, openPeerContextMenu: { _, _, _, _, _ in }, openMessageReplies: { _, _, _ in }, openReplyThreadOriginalMessage: { _ in }, openMessageStats: { _ in }, editMessageMedia: { _, _ in }, copyText: { _ in }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false }, getMessageTransitionNode: { return nil }, updateChoosingSticker: { _ in }, commitEmojiInteraction: { _, _, _, _ in }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _, _, _, _ in }, adContextAction: { _, _, _ in }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, openAdsInfo: { }, displayGiveawayParticipationStatus: { _ in }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in }, sendGift: { _ in }, openUniqueGift: { _ in }, openMessageFeeException: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { }, playShakeAnimation: { }, displayQuickShare: { _, _ ,_ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) var entryAttributes = ChatMessageEntryAttributes() entryAttributes.isCentered = isCentered let content: ChatMessageItemContent let chatLocation: ChatLocation if messages.count > 1 { content = .group(messages: messages.map { ($0, true, .none, entryAttributes, nil) }) chatLocation = .peer(id: messages.first!.id.peerId) } else { content = .message(message: messages.first!, read: true, selection: .none, attributes: entryAttributes, location: nil) chatLocation = .peer(id: messages.first!.id.peerId) } return ChatMessageItemImpl(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: isPreview), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, availableMessageEffects: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: accountPeer.flatMap(EnginePeer.init), forceInlineReactions: true, isStandalone: isStandalone), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { return ChatMessageDateHeader(timestamp: timestamp, scheduled: false, presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), controllerInteraction: nil, context: context) } public func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { return ChatMessageAvatarHeader(timestamp: timestamp, peerId: peer.id, peer: peer, messageReference: nil, message: message, presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, controllerInteraction: nil, storyStats: nil) } public func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = legacyWallpaperPicker(context: context, presentationData: presentationData).start(next: { generator in let legacyController = LegacyController(presentation: .navigation, theme: presentationData.theme) legacyController.navigationPresentation = .modal legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style let controller = generator(legacyController.context) legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] controller.selectionBlock = { [weak legacyController] asset, _ in if let asset = asset { let _ = (fetchPhotoLibraryImage(localIdentifier: asset.backingAsset.localIdentifier, thumbnail: false) |> deliverOnMainQueue).start(next: { imageAndFlag in if let (image, _) = imageAndFlag { completion(image) } }) if let legacyController = legacyController { legacyController.dismiss() } } } controller.dismissalBlock = { [weak legacyController] in if let legacyController = legacyController { legacyController.dismiss() } } present(legacyController) }) } public func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? { return makeInstantPageControllerImpl(context: context, message: message, sourcePeerType: sourcePeerType) } public func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController { return makeInstantPageControllerImpl(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) } public func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { openChatWallpaperImpl(context: context, message: message, present: present) } public func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController { return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: context.engine.privacy.webSessions(), websitesOnly: false) } public func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController { return ChatQrCodeScreen(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary)) } public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController { return SettingsUI.makePrivacyAndSecurityController(context: context) } public func makeBioPrivacyController(context: AccountContext, settings: Promise, present: @escaping (ViewController) -> Void) { SettingsUI.makeBioPrivacyController(context: context, settings: settings, present: present) } public func makeBirthdayPrivacyController(context: AccountContext, settings: Promise, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void) { SettingsUI.makeBirthdayPrivacyController(context: context, settings: settings, openedFromBirthdayScreen: openedFromBirthdayScreen, present: present) } public func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController { return SettingsUI.makeSetupTwoFactorAuthController(context: context) } public func makeStorageManagementController(context: AccountContext) -> ViewController { return StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { [weak context] category in guard let context else { return nil } return storageUsageExceptionsScreen(context: context, category: category) }) } public func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, send: send) } public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? { let inputPanelNode = LegacyMessageInputPanelNode( context: context, chatLocation: chatLocation, isScheduledMessages: isScheduledMessages, isFile: isFile, present: present, presentInGlobalOverlay: presentInGlobalOverlay, makeEntityInputView: { return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) } ) return inputPanelNode } public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController { return HashtagSearchController(context: context, peer: peer, query: query, mode: stories ? .chatOnly : .generic, stories: stories, forceDark: forceDark) } public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController { return StorySearchGridScreen(context: context, scope: scope, listContext: listContext) } public func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController { return PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: isArchive ? .archive : .saved) } public func makeArchiveSettingsController(context: AccountContext) -> ViewController { return archiveSettingsController(context: context) } public func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController { return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, scrollToTags: scrollToTags, dismissed: dismissed) } public func makeBusinessSetupScreen(context: AccountContext) -> ViewController { return PremiumIntroScreen(context: context, mode: .business, source: .settings, modal: false, forceDark: false) } public func makeChatbotSetupScreen(context: AccountContext, initialData: ChatbotSetupScreenInitialData) -> ViewController { return ChatbotSetupScreen(context: context, initialData: initialData as! ChatbotSetupScreen.InitialData) } public func makeChatbotSetupScreenInitialData(context: AccountContext) -> Signal { return ChatbotSetupScreen.initialData(context: context) } public func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController { return BusinessLocationSetupScreen(context: context, initialValue: initialValue, completion: completion) } public func makeBusinessHoursSetupScreen(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) -> ViewController { return BusinessHoursSetupScreen(context: context, initialValue: initialValue, completion: completion) } public func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController { return AutomaticBusinessMessageSetupScreen(context: context, initialData: initialData as! AutomaticBusinessMessageSetupScreen.InitialData, mode: isAwayMode ? .away : .greeting) } public func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal { return AutomaticBusinessMessageSetupScreen.initialData(context: context) } public func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController { return QuickReplySetupScreen(context: context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .manage) } public func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal { return QuickReplySetupScreen.initialData(context: context) } public func makeBusinessIntroSetupScreen(context: AccountContext, initialData: BusinessIntroSetupScreenInitialData) -> ViewController { return BusinessIntroSetupScreen(context: context, initialData: initialData as! BusinessIntroSetupScreen.InitialData) } public func makeBusinessIntroSetupScreenInitialData(context: AccountContext) -> Signal { return BusinessIntroSetupScreen.initialData(context: context) } public func makeBusinessLinksSetupScreen(context: AccountContext, initialData: BusinessLinksSetupScreenInitialData) -> ViewController { return BusinessLinksSetupScreen(context: context, initialData: initialData as! BusinessLinksSetupScreen.InitialData) } public func makeBusinessLinksSetupScreenInitialData(context: AccountContext) -> Signal { return BusinessLinksSetupScreen.makeInitialData(context: context) } public func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController { return CollectibleItemInfoScreen(context: context, initialData: initialData as! CollectibleItemInfoScreen.InitialData) } public func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal { return CollectibleItemInfoScreen.initialData(context: context, peerId: peerId, subject: subject) } public func makeBotSettingsScreen(context: AccountContext, peerId: EnginePeer.Id?) -> ViewController { if let peerId { return botSettingsScreen(context: context, peerId: peerId) } else { return botListSettingsScreen(context: context) } } private func mapIntroSource(source: PremiumIntroSource) -> PremiumSource { let mappedSource: PremiumSource switch source { case .settings: mappedSource = .settings case .stickers: mappedSource = .stickers case .reactions: mappedSource = .reactions case .ads: mappedSource = .ads case .upload: mappedSource = .upload case .groupsAndChannels: mappedSource = .groupsAndChannels case .pinnedChats: mappedSource = .pinnedChats case .publicLinks: mappedSource = .publicLinks case .savedGifs: mappedSource = .savedGifs case .savedStickers: mappedSource = .savedStickers case .folders: mappedSource = .folders case .chatsPerFolder: mappedSource = .chatsPerFolder case .appIcons: mappedSource = .appIcons case .accounts: mappedSource = .accounts case .about: mappedSource = .about case let .deeplink(reference): mappedSource = .deeplink(reference) case let .profile(peerId): mappedSource = .profile(peerId) case let .emojiStatus(peerId, fileId, file, packTitle): mappedSource = .emojiStatus(peerId, fileId, file, packTitle) case .voiceToText: mappedSource = .voiceToText case .fasterDownload: mappedSource = .fasterDownload case .translation: mappedSource = .translation case .stories: mappedSource = .stories case .storiesDownload: mappedSource = .storiesDownload case .storiesStealthMode: mappedSource = .storiesStealthMode case .storiesPermanentViews: mappedSource = .storiesPermanentViews case .storiesFormatting: mappedSource = .storiesFormatting case .storiesExpirationDurations: mappedSource = .storiesExpirationDurations case .storiesSuggestedReactions: mappedSource = .storiesSuggestedReactions case .storiesHigherQuality: mappedSource = .storiesHigherQuality case .storiesLinks: mappedSource = .storiesLinks case let .channelBoost(peerId): mappedSource = .channelBoost(peerId) case .nameColor: mappedSource = .nameColor case .similarChannels: mappedSource = .similarChannels case .wallpapers: mappedSource = .wallpapers case .presence: mappedSource = .presence case .readTime: mappedSource = .readTime case .messageTags: mappedSource = .messageTags case .folderTags: mappedSource = .folderTags case .messageEffects: mappedSource = .messageEffects case .animatedEmoji: mappedSource = .animatedEmoji case .paidMessages: mappedSource = .paidMessages case let .auth(price): mappedSource = .auth(price) } return mappedSource } public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { var modal = true if case .settings = source { modal = false } let controller = PremiumIntroScreen(context: context, source: self.mapIntroSource(source: source), modal: modal, forceDark: forceDark) controller.wasDismissed = dismissed return controller } public func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController { var modal = true if case .settings = source { modal = false } let controller = PremiumIntroScreen(screenContext: .sharedContext(sharedContext, engine, inAppPurchaseManager), source: self.mapIntroSource(source: source), modal: modal) controller.customProceed = proceed return controller } public func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var buttonText: String = presentationData.strings.Common_OK let mappedSubject: PremiumDemoScreen.Subject switch subject { case .doubleLimits: mappedSubject = .doubleLimits case .moreUpload: mappedSubject = .moreUpload case .fasterDownload: mappedSubject = .fasterDownload case .voiceToText: mappedSubject = .voiceToText case .noAds: mappedSubject = .noAds case .uniqueReactions: mappedSubject = .uniqueReactions case .premiumStickers: mappedSubject = .premiumStickers case .advancedChatManagement: mappedSubject = .advancedChatManagement case .profileBadge: mappedSubject = .profileBadge case .animatedUserpics: mappedSubject = .animatedUserpics case .appIcons: mappedSubject = .appIcons case .animatedEmoji: mappedSubject = .animatedEmoji case .emojiStatus: mappedSubject = .emojiStatus case .translation: mappedSubject = .translation case .stories: mappedSubject = .stories buttonText = presentationData.strings.Story_PremiumUpgradeStoriesButton case .colors: mappedSubject = .colors case .wallpapers: mappedSubject = .wallpapers case .messageTags: mappedSubject = .messageTags case .lastSeen: mappedSubject = .lastSeen case .messagePrivacy: mappedSubject = .messagePrivacy case .folderTags: mappedSubject = .folderTags case .messageEffects: mappedSubject = .messageEffects case .paidMessages: mappedSubject = .paidMessages case .business: mappedSubject = .business buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton default: mappedSubject = .doubleLimits } switch mappedSubject { case .stories, .business, .doubleLimits: let controller = PremiumLimitsListScreen(context: context, subject: mappedSubject, source: .other, order: [mappedSubject.perk], buttonText: buttonText, isPremium: false, forceDark: forceDark) controller.action = action if let dismissed { controller.disposed = dismissed } return controller default: return PremiumDemoScreen(context: context, subject: mappedSubject, forceDark: forceDark, action: action) } } public func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController { let mappedSubject: PremiumLimitScreen.Subject switch subject { case .folders: mappedSubject = .folders case .chatsPerFolder: mappedSubject = .chatsPerFolder case .pins: mappedSubject = .pins case .files: mappedSubject = .files case .accounts: mappedSubject = .accounts case .linksPerSharedFolder: mappedSubject = .linksPerSharedFolder case .membershipInSharedFolders: mappedSubject = .membershipInSharedFolders case .channels: mappedSubject = .channels case .expiringStories: mappedSubject = .expiringStories case .multiStories: mappedSubject = .multiStories case .storiesWeekly: mappedSubject = .storiesWeekly case .storiesMonthly: mappedSubject = .storiesMonthly case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, myBoostCount, canBoostAgain): mappedSubject = .storiesChannelBoost(peer: peer, boostSubject: .stories, isCurrent: isCurrent, level: level, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: link, myBoostCount: myBoostCount, canBoostAgain: canBoostAgain) } return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } public func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var presentBirthdayPickerImpl: (() -> Void)? let starsMode: ContactSelectionControllerMode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: false, selfSubtitle: nil) let contactOptions: Signal<[ContactListAdditionalOption], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) |> map { birthday in if birthday == nil { return [ContactListAdditionalOption( title: presentationData.strings.Premium_Gift_ContactSelection_AddBirthday, icon: .generic(UIImage(bundleImageName: "Contact List/AddBirthdayIcon")!), action: { presentBirthdayPickerImpl?() }, clearHighlightAutomatically: true )] } else { return [] } } |> deliverOnMainQueue let options = Promise<[StarsGiftOption]>() options.set(context.engine.payments.starsGiftOptions(peerId: nil)) let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, mode: starsMode, autoDismiss: false, title: { strings in return strings.Stars_Purchase_GiftStars }, options: contactOptions )) let _ = (controller.result |> deliverOnMainQueue).start(next: { result in if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { completion([peer.id]) } }) presentBirthdayPickerImpl = { [weak controller] in guard let controller else { return } let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.setupBirthday.id).startStandalone() let settingsPromise: Promise if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() { settingsPromise = current } else { settingsPromise = Promise() settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) } let birthdayController = BirthdayPickerScreen(context: context, settings: settingsPromise.get(), openSettings: { context.sharedContext.makeBirthdayPrivacyController(context: context, settings: settingsPromise, openedFromBirthdayScreen: true, present: { [weak controller] c in controller?.push(c) }) }, completion: { [weak controller] value in let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone() controller?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in return true }), in: .current) }) controller.push(birthdayController) } return controller } public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var presentExportAlertImpl: (() -> Void)? var presentTransferAlertImpl: ((EnginePeer) -> Void)? var presentBirthdayPickerImpl: (() -> Void)? var mode: ContactSelectionControllerMode = .generic var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? if case let .starGiftTransfer(birthdays, _, _, _, _, showSelf) = source { mode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: showSelf, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_TransferSelf) currentBirthdays = birthdays } else if case let .chatList(birthdays) = source { mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf) currentBirthdays = birthdays } else if case let .settings(birthdays) = source { mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf) currentBirthdays = birthdays } else { mode = .starsGifting(birthdays: nil, hasActions: true, showSelf: false, selfSubtitle: nil) } var allowChannelsInSearch = false var isChannelGift = false let contactOptions: Signal<[ContactListAdditionalOption], NoError> if case let .starGiftTransfer(_, reference, _, _, canExportDate, _) = source { allowChannelsInSearch = true if case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel { isChannelGift = true } var subtitle: String? if let canExportDate { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if currentTime > canExportDate { subtitle = nil } else { let delta = canExportDate - currentTime let days: Int32 = Int32(ceil(Float(delta) / 86400.0)) let daysString = presentationData.strings.Gift_Transfer_SendUnlocks_Days(days) subtitle = presentationData.strings.Gift_Transfer_SendUnlocks(daysString).string } contactOptions = .single([ ContactListAdditionalOption( title: presentationData.strings.Gift_Transfer_SendViaBlockchain, subtitle: subtitle, icon: .generic(UIImage(bundleImageName: "Item List/Ton")!), style: .generic, action: { presentExportAlertImpl?() }, clearHighlightAutomatically: true ) ]) } else { contactOptions = .single([]) } } else if currentBirthdays != nil || "".isEmpty { contactOptions = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) |> map { birthday in if birthday == nil { return [ContactListAdditionalOption( title: presentationData.strings.Premium_Gift_ContactSelection_AddBirthday, icon: .generic(UIImage(bundleImageName: "Contact List/AddBirthdayIcon")!), action: { presentBirthdayPickerImpl?() }, clearHighlightAutomatically: true )] } else { return [] } } |> deliverOnMainQueue } else { contactOptions = .single([]) } var openProfileImpl: ((EnginePeer) -> Void)? var sendMessageImpl: ((EnginePeer) -> Void)? let title: String if case .starGiftTransfer = source { title = presentationData.strings.Gift_Transfer_Title } else { title = presentationData.strings.Gift_PremiumOrStars_Title } let options = Promise<[PremiumGiftCodeOption]>() options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, mode: mode, autoDismiss: false, title: { _ in return title }, options: contactOptions, allowChannelsInSearch: allowChannelsInSearch, openProfile: { peer in openProfileImpl?(peer) }, sendMessage: { peer in sendMessageImpl?(peer) } )) controller.navigationPresentation = .modal let _ = (combineLatest( queue: Queue.mainQueue(), controller.result, options.get() |> distinctUntilChanged )).startStandalone(next: { [weak controller] result, options in if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { if case .starGiftTransfer = source { presentTransferAlertImpl?(EnginePeer(peer)) } else { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.DisallowedGifts(id: peer.id)) |> deliverOnMainQueue).start(next: { disallowedGifts in if let disallowedGifts, disallowedGifts == TelegramDisallowedGifts.All && peer.id != context.account.peerId { let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Send_GiftsDisallowed(EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) controller?.present(alertController, in: .window(.root)) return } let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions, hasBirthday: currentBirthdays?[peer.id] != nil) giftController.navigationPresentation = .modal controller?.push(giftController) }) } } }) sendMessageImpl = { [weak self, weak controller] peer in guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { return } self.navigateToChatController( NavigateToChatControllerParams( navigationController: navigationController, context: context, chatLocation: .peer(peer) ) ) } openProfileImpl = { [weak self, weak controller] peer in guard let self, let controller else { return } if let infoController = self.makePeerInfoController( context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.smallProfileImage != nil, fromChat: false, requestsContext: nil ) { controller.replace(with: infoController) } } presentBirthdayPickerImpl = { [weak controller] in guard let controller else { return } let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.setupBirthday.id).startStandalone() let settingsPromise: Promise if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() { settingsPromise = current } else { settingsPromise = Promise() settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) } let birthdayController = BirthdayPickerScreen(context: context, settings: settingsPromise.get(), openSettings: { context.sharedContext.makeBirthdayPrivacyController(context: context, settings: settingsPromise, openedFromBirthdayScreen: true, present: { [weak controller] c in controller?.push(c) }) }, completion: { [weak controller] value in let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone() controller?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in return true }), in: .current) }) controller.push(birthdayController) } presentExportAlertImpl = { [weak controller] in guard let controller, case let .starGiftTransfer(_, reference, gift, _, canExportDate, _) = source, let canExportDate else { return } let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if currentTime > canExportDate { let alertController = giftWithdrawAlertController(context: context, gift: gift, commit: { let _ = (context.engine.payments.checkStarGiftWithdrawalAvailability(reference: reference) |> deliverOnMainQueue).start(error: { [weak controller] error in switch error { case .serverProvided: return case .requestPassword: let alertController = confirmGiftWithdrawalController(context: context, reference: reference, present: { [weak controller] c, a in controller?.present(c, in: .window(.root)) }, completion: { [weak controller] url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) guard let controller, let navigationController = controller.navigationController as? NavigationController else { return } var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is ContactSelectionController) } navigationController.setViewControllers(controllers, animated: true) }) controller?.present(alertController, in: .window(.root)) default: let alertController = giftWithdrawalController(context: context, reference: reference, initialError: error, present: { [weak controller] c, a in controller?.present(c, in: .window(.root)) }, completion: { _ in }) controller?.present(alertController, in: .window(.root)) } }) }) controller.present(alertController, in: .window(.root)) } else { let delta = canExportDate - currentTime let days: Int32 = Int32(ceil(Float(delta) / 86400.0)) let daysString = presentationData.strings.Gift_Transfer_UnlockPending_Text_Days(days) let title = presentationData.strings.Gift_Transfer_UnlockPending_Title let text = presentationData.strings.Gift_Transfer_UnlockPending_Text(daysString).string let alertController = textAlertController(context: context, title: title, text: text, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) ]) controller.present(alertController, in: .window(.root)) } } let optionsPromise = Promise<[StarsTopUpOption]?>(nil) if let state = context.starsContext?.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { optionsPromise.set(context.engine.payments.starsTopUpOptions() |> map(Optional.init)) } presentTransferAlertImpl = { [weak controller] peer in guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _, _) = source else { return } var dismissAlertImpl: (() -> Void)? let alertController = giftTransferAlertController( context: context, gift: gift, peer: peer, transferStars: transferStars, navigationController: controller.navigationController as? NavigationController, commit: { [weak controller] in let proceed: (Bool) -> Void = { waitForTopUp in guard let controller, let navigationController = controller.navigationController as? NavigationController else { return } if let completion { let _ = (completion([peer.id]) |> deliverOnMainQueue).startStandalone(error: { [weak navigationController] error in guard let navigationController else { return } dismissAlertImpl?() var errorText: String? switch error { case .disallowedStarGift: errorText = presentationData.strings.Gift_Send_ErrorDisallowed(peer.compactDisplayTitle).string default: errorText = presentationData.strings.Gift_Send_ErrorUnknown } if let errorText = errorText { let alertController = textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true) if let lastController = navigationController.viewControllers.last as? ViewController { lastController.present(alertController, in: .window(.root)) } } }, completed: { [weak navigationController] in guard let navigationController else { return } dismissAlertImpl?() var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is ContactSelectionController) } if !isChannelGift { if peer.id.namespace == Namespaces.Peer.CloudChannel { if let controller = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .gifts, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil ) { controllers.append(controller) } } else { var foundController = false for controller in controllers.reversed() { if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { chatController.hintPlayNextOutgoingGift() foundController = true break } } if !foundController { let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) chatController.hintPlayNextOutgoingGift() controllers.append(chatController) } } } navigationController.setViewControllers(controllers, animated: true) Queue.mainQueue().after(0.3) { let tooltipController = UndoOverlayController( presentationData: presentationData, content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), elevatedLayout: false, action: { _ in return true } ) if let lastController = navigationController.viewControllers.last as? ViewController { lastController.present(tooltipController, in: .window(.root)) } Queue.mainQueue().after(0.5) { var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is GiftViewScreen) } navigationController.setViewControllers(controllers, animated: false) } } }) } } if transferStars > 0, let starsContext = context.starsContext, let starsState = starsContext.currentState { if starsState.balance < StarsAmount(value: transferStars, nanos: 0) { let _ = (optionsPromise.get() |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] options in let purchaseController = context.sharedContext.makeStarsPurchaseScreen( context: context, starsContext: starsContext, options: options ?? [], purpose: .transferStarGift(requiredStars: transferStars), completion: { stars in starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) proceed(true) } ) controller?.push(purchaseController) }) } else { proceed(false) } } else { proceed(false) } } ) controller.present(alertController, in: .current) dismissAlertImpl = { [weak alertController] in alertController?.dismissAnimated() } } return controller } public func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController { guard let starsContext = context.starsContext else { fatalError() } let controller = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: hasBirthday, completion: completion) controller.navigationPresentation = .modal return controller } public func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController { guard let starsContext = context.starsContext else { fatalError() } let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift) return controller } public func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController { let mappedSubject: PremiumPrivacyScreen.Subject let introSource: PremiumIntroSource switch subject { case .presence: mappedSubject = .presence introSource = .presence case .readTime: mappedSubject = .readTime introSource = .presence } var actionImpl: (() -> Void)? var openPremiumIntroImpl: (() -> Void)? let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = PremiumPrivacyScreen( context: context, peerId: peerId, subject: mappedSubject, action: { actionImpl?() }, openPremiumIntro: { openPremiumIntroImpl?() } ) actionImpl = { [weak controller] in guard let parentController = controller, let navigationController = parentController.navigationController as? NavigationController else { return } let currentPrivacy = Promise() currentPrivacy.set(context.engine.privacy.requestAccountPrivacySettings()) let tooltipText: String switch subject { case .presence: tooltipText = presentationData.strings.Settings_Privacy_LastSeenRevealedToast let _ = (currentPrivacy.get() |> take(1) |> mapToSignal { current in let presence = current.presence var disabledFor: [PeerId: SelectivePrivacyPeer] = [:] switch presence { case let .enableEveryone(disabledForValue), let .enableContacts(_, disabledForValue, _, _): disabledFor = disabledForValue default: break } disabledFor.removeValue(forKey: peerId) return context.engine.privacy.updateSelectiveAccountPrivacySettings(type: .presence, settings: .enableEveryone(disableFor: disabledFor)) } |> deliverOnMainQueue).startStandalone(completed: { [weak navigationController] in let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peerId).startStandalone() if let parentController = navigationController?.viewControllers.last as? ViewController { parentController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: tooltipText, timeout: 4.0, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .window(.root)) } }) case .readTime: tooltipText = presentationData.strings.Settings_Privacy_MessageReadTimeRevealedToast let _ = (currentPrivacy.get() |> take(1) |> mapToSignal { current in var settings = current.globalSettings settings.hideReadTime = false return context.engine.privacy.updateGlobalPrivacySettings(settings: settings) } |> deliverOnMainQueue).startStandalone(completed: { [weak navigationController] in if let parentController = navigationController?.viewControllers.last as? ViewController { parentController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: tooltipText, timeout: 4.0, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .window(.root)) } }) } } openPremiumIntroImpl = { [weak controller] in guard let parentController = controller else { return } let controller = context.sharedContext.makePremiumIntroController(context: context, source: introSource, forceDark: false, dismissed: nil) parentController.push(controller) } return controller } public func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) var pushImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? let controller = PremiumBoostLevelsScreen( context: context, peerId: peerId, mode: .owner(subject: subject), status: boostStatus, myBoostStatus: myBoostStatus, openStats: openStats, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { var updatedPresentationData: (initial: PresentationData, signal: Signal)? if forceDark { let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) updatedPresentationData = (presentationData, .single(presentationData)) } let controller = createGiveawayController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, subject: .generic) pushImpl?(controller) Queue.mainQueue().after(0.4) { dismissImpl?() } } : nil, forceDark: forceDark ) pushImpl = { [weak controller] c in controller?.push(c) } dismissImpl = { [weak controller] in if let controller, let navigationController = controller.navigationController as? NavigationController { navigationController.setViewControllers(navigationController.viewControllers.filter { !($0 is PremiumBoostLevelsScreen) }, animated: false) } } return controller } public func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, actionPerformed: ((Bool) -> Void)?) -> ViewController { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, actionTitle: actionTitle, isEditing: isEditing, expandIfNeeded: expandIfNeeded, parentNavigationController: parentNavigationController, sendSticker: sendSticker, actionPerformed: { actions in if let (_, _, action) = actions.first { switch action { case .add: actionPerformed?(true) case .remove: actionPerformed?(false) } } }) } public func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController { let subject: Signal if let asset = source as? PHAsset { subject = .single(.asset(asset)) } else if let image = source as? UIImage { subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight)) } else { subject = .single(.empty(PixelDimensions(width: 1080, height: 1920))) } let editorController = MediaEditorScreenImpl( context: context, mode: .botPreview, subject: subject, customTarget: nil, transitionIn: transitionArguments.flatMap { .gallery( MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( sourceView: $0.0, sourceRect: $0.1, sourceImage: $0.2 ) ) }, transitionOut: { finished, isNew in if !finished, let transitionArguments { return MediaEditorScreenImpl.TransitionOut( destinationView: transitionArguments.0, destinationRect: transitionArguments.0.bounds, destinationCornerRadius: 0.0 ) } else if finished, let transitionOut = transitionOut(), let destinationView = transitionOut.destinationView { return MediaEditorScreenImpl.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, destinationCornerRadius: transitionOut.destinationCornerRadius, completion: transitionOut.completion ) } return nil }, completion: { results, commit in completion(results.first!, commit) } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) editorController.cancelled = { _ in cancelled() } return editorController } public func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController { let subject: Signal var mode: MediaEditorScreenImpl.Mode.StickerEditorMode var fromCamera = false if let (file, emoji) = source as? (TelegramMediaFile, [String]) { subject = .single(.sticker(file, emoji)) mode = .editing } else if let asset = source as? PHAsset { subject = .single(.asset(asset)) mode = .addingToPack } else if let image = source as? UIImage { subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight)) mode = .addingToPack } else if let source = source as? Signal { subject = source |> map { value -> MediaEditorScreenImpl.Subject? in switch value { case .pendingImage: return nil case let .image(image): return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft) default: return nil } } fromCamera = true mode = .addingToPack } else { subject = .single(.empty(PixelDimensions(width: 1080, height: 1920))) mode = .addingToPack } if intro { mode = .businessIntro } let editorController = MediaEditorScreenImpl( context: context, mode: .stickerEditor(mode: mode), subject: subject, transitionIn: fromCamera ? .camera : transitionArguments.flatMap { .gallery( MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( sourceView: $0.0, sourceRect: $0.1, sourceImage: $0.2 ) ) }, transitionOut: { finished, isNew in if !finished, let transitionArguments { return MediaEditorScreenImpl.TransitionOut( destinationView: transitionArguments.0, destinationRect: transitionArguments.0.bounds, destinationCornerRadius: 0.0 ) } return nil }, completion: { results, commit in if case let .sticker(file, emoji) = results.first?.media { completion(file, emoji, { commit({}) }) } } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) editorController.cancelled = { _ in cancelled() } return editorController } public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController { let subject: Signal if let image = source as? UIImage { subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight)) } else if let path = source as? String { subject = .single(.video(videoPath: path, thumbnail: nil, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: PixelDimensions(width: 1080, height: 1920), duration: 0.0, videoPositionChanges: [], additionalVideoPosition: .bottomRight)) } else { subject = .single(.empty(PixelDimensions(width: 1080, height: 1920))) } let editorController = MediaEditorScreenImpl( context: context, mode: .storyEditor(remainingCount: 1), subject: subject, customTarget: nil, initialCaption: text.flatMap { NSAttributedString(string: $0) }, initialLink: link, transitionIn: nil, transitionOut: { finished, isNew in return nil }, completion: { results, commit in completion(results.first!, commit) } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) return editorController } public func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController { return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion) } public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController { return storyMediaPickerController(context: context, isDark: isDark, forCollage: forCollage, selectionLimit: selectionLimit, getSourceRect: getSourceRect, completion: completion, multipleCompletion: multipleCompletion, dismissed: dismissed, groupsPresented: groupsPresented) } public func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController { return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed) } public func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController { return avatarMediaPickerController(context: context, getSourceRect: getSourceRect, canDelete: canDelete, performDelete: performDelete, completion: completion, dismissed: dismissed) } public func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController { let controller = StickerPickerScreen(context: context, inputData: inputData.get(), expanded: true, hasGifs: false, hasInteractiveStickers: false) controller.completion = { content in if let content, case let .file(file, _) = content { completion(file) } return true } return controller } public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { return proxySettingsController(accountManager: sharedContext.accountManager, sharedContext: sharedContext, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData) } public func makeDataAndStorageController(context: AccountContext, sensitiveContent: Bool) -> ViewController { return dataAndStorageController(context: context, focusOnItemTag: sensitiveContent ? DataAndStorageEntryTag.sensitiveContent : nil) } public func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController { return installedStickerPacksController(context: context, mode: mode, forceTheme: forceTheme) } public func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController { return channelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, section: boosts ? .boosts : .stats, boostStatus: boostStatus) } public func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, messageId: EngineMessage.Id) -> ViewController { return messageStatsController(context: context, updatedPresentationData: updatedPresentationData, subject: .message(id: messageId)) } public func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController { return messageStatsController(context: context, updatedPresentationData: updatedPresentationData, subject: .story(peerId: peerId, id: storyId, item: storyItem, fromStory: fromStory)) } public func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController { return StarsTransactionsScreen(context: context, starsContext: starsContext) } public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController { return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, completion: completion) } public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController { return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion) } public func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController { return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .starsChatSubscription(hash: link), extendedMedia: [], inputData: inputData, navigateToPeer: navigateToPeer, completion: { _ in }) } public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController { return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer)) } public func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController { return StarsTransactionScreen(context: context, subject: .receipt(receipt)) } public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController { return StarsTransactionScreen(context: context, subject: .subscription(subscription), updateSubscription: update) } public func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController { return StarsTransactionScreen(context: context, subject: .importer(peer, pricing, importer, usdRate)) } public func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController { return StarsStatisticsScreen(context: context, peerId: peerId, revenueContext: revenueContext) } public func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController { return StarsWithdrawScreen(context: context, mode: .paidMedia(initialValue), completion: completion) } public func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController { return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion) } public func makeStarsWithdrawalScreen(context: AccountContext, subject: StarsWithdrawalScreenSubject, completion: @escaping (Int64) -> Void) -> ViewController { let mode: StarsWithdrawScreen.Mode switch subject { case .withdraw: mode = .accountWithdraw case let .enterAmount(current, minValue, fractionAfterCommission, kind): mode = .paidMessages(current: current.value, minValue: minValue.value, fractionAfterCommission: fractionAfterCommission, kind: kind) } return StarsWithdrawScreen(context: context, mode: mode, completion: completion) } public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController { return StarsWithdrawScreen(context: context, mode: .starGiftResell(update), completion: completion) } public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { return StarsTransactionScreen(context: context, subject: .gift(message)) } public func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController { return StarsTransactionScreen(context: context, subject: .boost(peerId, boost)) } public func makeStarsIntroScreen(context: AccountContext) -> ViewController { return StarsIntroScreen(context: context) } public func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController { return GiftViewScreen(context: context, subject: .message(message), shareStory: shareStory) } public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController { let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory) controller.disposed = { dismissed?() } return controller } public func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController { let controller = GiftViewScreen(context: context, subject: .wearPreview(gift)) return controller } public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { let editorSubject: Signal switch subject { case let .messages(messages): editorSubject = .single(.message(messages.map { $0.id })) case let .gift(gift): editorSubject = .single(.gift(gift)) } let externalState = MediaEditorTransitionOutExternalState( storyTarget: nil, isForcedTarget: false, isPeerArchived: false, transitionOut: nil ) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = MediaEditorScreenImpl( context: context, mode: .storyEditor(remainingCount: 1), subject: editorSubject, transitionIn: nil, transitionOut: { _, _ in return nil }, completion: { [weak parentController] results, commit in guard let result = results.first else { return } let targetPeerId: EnginePeer.Id let target: Stories.PendingTarget if let sendAsPeerId = result.options.sendAsPeerId { target = .peer(sendAsPeerId) targetPeerId = sendAsPeerId } else { target = .myStories targetPeerId = context.account.peerId } externalState.storyTarget = target if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) } let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer else { return } let text: String if case .channel = peer { text = presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string } else { text = presentationData.strings.Story_MessageReposted_Personal } Queue.mainQueue().after(0.25) { parentController?.present(UndoOverlayController( presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false } ), in: .current) Queue.mainQueue().after(0.1) { HapticFeedback().success() } } }) } ) return controller } public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) { let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil) |> deliverOnMainQueue).startStandalone(next: { result in if case let .options(title, options) = result { present(ContentReportScreen(context: context, subject: subject, title: title, options: options, forceDark: forceDark, completed: completion, requestSelectMessages: requestSelectMessages)) } }) } public func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController { let controller = ShareController(context: context, subject: subject, externalShare: forceExternal) controller.shareStory = shareStory controller.enqueued = enqueued controller.actionCompleted = actionCompleted return controller } public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { return MiniAppListScreen.initialData(context: context) } public func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController { return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) } public func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController { return incomingMessagePrivacyScreen(context: context, value: value, exceptions: exceptions, update: update) } public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) } public func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal { return AffiliateProgramSetupScreen.content(context: context, peerId: peerId, mode: mode) } public func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController { return AffiliateProgramSetupScreen(context: context, initialContent: initialData) } public func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController { return JoinAffiliateProgramScreen(context: context, sourcePeer: sourcePeer, commissionPermille: commissionPermille, programDuration: programDuration, revenuePerUser: revenuePerUser, mode: mode) } public func makeJoinSubjectScreen(context: AccountContext, mode: JoinSubjectScreenMode) -> ViewController { return JoinSubjectScreen(context: context, mode: mode) } public func makeOldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void) -> ViewController { return oldChannelsController(context: context, updatedPresentationData: updatedPresentationData, intent: intent, completed: completed) } public func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController { let controller = GalleryController(context: context, source: source, streamSingleVideo: streamSingleVideo, replaceRootController: { _, _ in }, baseNavigationController: nil) if isPreview { controller.setHintWillBePresentedInPreviewingContext(true) } return controller } public func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController { return AccountFreezeInfoScreen(context: context) } public func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController { return SendInviteLinkScreen(context: context, subject: subject, peers: peers, theme: theme) } public func makePostSuggestionsSettingsScreen(context: AccountContext) -> ViewController { return PostSuggestionsSettingsScreen(context: context, completion: {}) } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { if let _ = peer as? TelegramGroup { return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } else if let _ = peer as? TelegramChannel { var forumTopicThread: ChatReplyThreadMessage? var switchToRecommendedChannels = false var switchToGifts = false var switchToGroupsInCommon = false switch mode { case let .forumTopic(thread): forumTopicThread = thread case .recommendedChannels: switchToRecommendedChannels = true case .gifts: switchToGifts = true case .groupsInCommon: switchToGroupsInCommon = true default: break } return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], forumTopicThread: forumTopicThread, switchToRecommendedChannels: switchToRecommendedChannels, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon) } else if peer is TelegramUser { var nearbyPeerDistance: Int32? var reactionSourceMessageId: MessageId? var callMessages: [Message] = [] var hintGroupInCommon: PeerId? var forumTopicThread: ChatReplyThreadMessage? var isMyProfile = false var switchToGifts = false var switchToGroupsInCommon = false switch mode { case let .nearbyPeer(distance): nearbyPeerDistance = distance case let .calls(messages): callMessages = messages case .generic: break case let .group(id): hintGroupInCommon = id case let .reaction(messageId): reactionSourceMessageId = messageId case let .forumTopic(thread): forumTopicThread = thread case .myProfile: isMyProfile = true case .gifts: switchToGifts = true case .myProfileGifts: isMyProfile = true switchToGifts = true case .groupsInCommon: switchToGroupsInCommon = true default: break } return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon) } else if peer is TelegramSecretChat { return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } return nil } private func useFlatModalCallsPresentation(context: AccountContext) -> Bool { if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_modalcalls"] != nil { return false } return true }