Files
GLEGram-iOS/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift
T
Leeksov 4647310322 GLEGram 12.5 — Initial public release
Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
2026-04-06 09:48:12 +03:00

608 lines
29 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import TelegramNotices
import UndoUI
private final class DataPrivacyControllerArguments {
let account: Account
let clearPaymentInfo: () -> Void
let updateSecretChatLinkPreviews: (Bool) -> Void
let deleteContacts: () -> Void
let updateSyncContacts: (Bool) -> Void
let updateSuggestFrequentContacts: (Bool) -> Void
let deleteCloudDrafts: () -> Void
let openBotListSettings: () -> Void
init(account: Account, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void, deleteCloudDrafts: @escaping () -> Void, openBotListSettings: @escaping () -> Void) {
self.account = account
self.clearPaymentInfo = clearPaymentInfo
self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews
self.deleteContacts = deleteContacts
self.updateSyncContacts = updateSyncContacts
self.updateSuggestFrequentContacts = updateSuggestFrequentContacts
self.deleteCloudDrafts = deleteCloudDrafts
self.openBotListSettings = openBotListSettings
}
}
private enum DataPrivacySection: Int32 {
case contacts
case frequentContacts
case chats
case payments
case secretChats
case bots
}
public enum DataPrivacyEntryTag: ItemListItemTag, Equatable {
case deleteSynced
case syncContacts
case suggestContacts
case deleteCloudDrafts
case clearPaymentInfo
case linkPreviews
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? DataPrivacyEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum DataPrivacyEntry: ItemListNodeEntry {
case contactsHeader(PresentationTheme, String)
case deleteContacts(PresentationTheme, String, Bool)
case syncContacts(PresentationTheme, String, Bool)
case syncContactsInfo(PresentationTheme, String)
case frequentContacts(PresentationTheme, String, Bool)
case frequentContactsInfo(PresentationTheme, String)
case chatsHeader(PresentationTheme, String)
case deleteCloudDrafts(PresentationTheme, String, Bool)
case paymentHeader(PresentationTheme, String)
case clearPaymentInfo(PresentationTheme, String, Bool)
case paymentInfo(PresentationTheme, String)
case secretChatLinkPreviewsHeader(PresentationTheme, String)
case secretChatLinkPreviews(PresentationTheme, String, Bool)
case secretChatLinkPreviewsInfo(PresentationTheme, String)
case botList
var section: ItemListSectionId {
switch self {
case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo:
return DataPrivacySection.contacts.rawValue
case .frequentContacts, .frequentContactsInfo:
return DataPrivacySection.frequentContacts.rawValue
case .chatsHeader, .deleteCloudDrafts:
return DataPrivacySection.chats.rawValue
case .paymentHeader, .clearPaymentInfo, .paymentInfo:
return DataPrivacySection.payments.rawValue
case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo:
return DataPrivacySection.secretChats.rawValue
case .botList:
return DataPrivacySection.bots.rawValue
}
}
var stableId: Int32 {
switch self {
case .contactsHeader:
return 0
case .deleteContacts:
return 1
case .syncContacts:
return 2
case .syncContactsInfo:
return 3
case .frequentContacts:
return 4
case .frequentContactsInfo:
return 5
case .chatsHeader:
return 6
case .deleteCloudDrafts:
return 7
case .paymentHeader:
return 8
case .clearPaymentInfo:
return 9
case .paymentInfo:
return 10
case .secretChatLinkPreviewsHeader:
return 11
case .secretChatLinkPreviews:
return 12
case .secretChatLinkPreviewsInfo:
return 13
case .botList:
return 14
}
}
static func ==(lhs: DataPrivacyEntry, rhs: DataPrivacyEntry) -> Bool {
switch lhs {
case let .contactsHeader(lhsTheme, lhsText):
if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .deleteContacts(lhsTheme, lhsText, lhsEnabled):
if case let .deleteContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .syncContacts(lhsTheme, lhsText, lhsEnabled):
if case let .syncContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .syncContactsInfo(lhsTheme, lhsText):
if case let .syncContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .frequentContacts(lhsTheme, lhsText, lhsEnabled):
if case let .frequentContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .frequentContactsInfo(lhsTheme, lhsText):
if case let .frequentContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chatsHeader(lhsTheme, lhsText):
if case let .chatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .deleteCloudDrafts(lhsTheme, lhsText, lhsEnabled):
if case let .deleteCloudDrafts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .paymentHeader(lhsTheme, lhsText):
if case let .paymentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .clearPaymentInfo(lhsTheme, lhsText, lhsEnabled):
if case let .clearPaymentInfo(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .paymentInfo(lhsTheme, lhsText):
if case let .paymentInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .secretChatLinkPreviewsHeader(lhsTheme, lhsText):
if case let .secretChatLinkPreviewsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .secretChatLinkPreviews(lhsTheme, lhsText, lhsEnabled):
if case let .secretChatLinkPreviews(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .secretChatLinkPreviewsInfo(lhsTheme, lhsText):
if case let .secretChatLinkPreviewsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case .botList:
if case .botList = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: DataPrivacyEntry, rhs: DataPrivacyEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DataPrivacyControllerArguments
switch self {
case let .contactsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .deleteContacts(_, text, value):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteContacts()
}, tag: DataPrivacyEntryTag.deleteSynced)
case let .syncContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSyncContacts(updatedValue)
}, tag: DataPrivacyEntryTag.syncContacts)
case let .syncContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .frequentContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSuggestFrequentContacts(updatedValue)
}, tag: DataPrivacyEntryTag.suggestContacts)
case let .frequentContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .chatsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .deleteCloudDrafts(_, text, value):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteCloudDrafts()
}, tag: DataPrivacyEntryTag.deleteCloudDrafts)
case let .paymentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .clearPaymentInfo(_, text, enabled):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.clearPaymentInfo()
}, tag: DataPrivacyEntryTag.clearPaymentInfo)
case let .paymentInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .secretChatLinkPreviewsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .secretChatLinkPreviews(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSecretChatLinkPreviews(updatedValue)
}, tag: DataPrivacyEntryTag.linkPreviews)
case let .secretChatLinkPreviewsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case .botList:
return ItemListDisclosureItem(
presentationData: presentationData,
systemStyle: .glass,
title: presentationData.strings.Settings_BotListSettings,
label: "",
sectionId: self.section,
style: .blocks,
action: {
arguments.openBotListSettings()
}
)
}
}
}
private struct DataPrivacyControllerState: Equatable {
var clearingPaymentInfo: Bool = false
var deletingContacts: Bool = false
var updatedSuggestFrequentContacts: Bool? = nil
var deletingCloudDrafts: Bool = false
}
private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [DataPrivacyEntry] {
var entries: [DataPrivacyEntry] = []
entries.append(.contactsHeader(presentationData.theme, presentationData.strings.Privacy_ContactsTitle))
entries.append(.deleteContacts(presentationData.theme, presentationData.strings.Privacy_ContactsReset, !state.deletingContacts))
entries.append(.syncContacts(presentationData.theme, presentationData.strings.Privacy_ContactsSync, synchronizeDeviceContacts))
entries.append(.syncContactsInfo(presentationData.theme, presentationData.strings.Privacy_ContactsSyncHelp))
entries.append(.frequentContacts(presentationData.theme, presentationData.strings.Privacy_TopPeers, frequentContacts))
entries.append(.frequentContactsInfo(presentationData.theme, presentationData.strings.Privacy_TopPeersHelp))
entries.append(.chatsHeader(presentationData.theme, presentationData.strings.Privacy_ChatsTitle))
entries.append(.deleteCloudDrafts(presentationData.theme, presentationData.strings.Privacy_DeleteDrafts, !state.deletingCloudDrafts))
entries.append(.paymentHeader(presentationData.theme, presentationData.strings.Privacy_PaymentsTitle))
entries.append(.clearPaymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfo, !state.clearingPaymentInfo))
entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoHelp))
entries.append(.secretChatLinkPreviewsHeader(presentationData.theme, presentationData.strings.Privacy_SecretChatsTitle))
entries.append(.secretChatLinkPreviews(presentationData.theme, presentationData.strings.Privacy_SecretChatsLinkPreviews, secretChatLinkPreviews ?? true))
entries.append(.secretChatLinkPreviewsInfo(presentationData.theme, presentationData.strings.Privacy_SecretChatsLinkPreviewsHelp))
if hasBotSettings {
entries.append(.botList)
}
return entries
}
public func dataPrivacyController(context: AccountContext, focusOnItemTag: DataPrivacyEntryTag? = nil) -> ViewController {
let statePromise = ValuePromise(DataPrivacyControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: DataPrivacyControllerState())
let updateState: ((DataPrivacyControllerState) -> DataPrivacyControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let currentInfoDisposable = MetaDisposable()
actionsDisposable.add(currentInfoDisposable)
let clearPaymentInfoDisposable = MetaDisposable()
actionsDisposable.add(clearPaymentInfoDisposable)
let arguments = DataPrivacyControllerArguments(account: context.account, clearPaymentInfo: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
var values = [true, true]
let toggleCheck: (Int) -> Void = { [weak controller] itemIndex in
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
if let item = item as? ActionSheetCheckboxItem {
values[itemIndex] = !item.value
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
}
return item
})
controller?.updateItem(groupIndex: 0, itemIndex: 2, { item in
if let item = item as? ActionSheetButtonItem {
let disabled = !values[0] && !values[1]
return ActionSheetButtonItem(title: item.title, color: disabled ? .disabled : .accent, enabled: !disabled, action: item.action)
}
return item
})
}
var items: [ActionSheetItem] = []
items.append(ActionSheetCheckboxItem(title: presentationData.strings.Privacy_PaymentsClear_PaymentInfo, label: "", value: true, action: { value in
toggleCheck(0)
}))
items.append(ActionSheetCheckboxItem(title: presentationData.strings.Privacy_PaymentsClear_ShippingInfo, label: "", value: true, action: { value in
toggleCheck(1)
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_ClearNone, action: {
var clear = false
updateState { state in
var state = state
if !state.clearingPaymentInfo {
clear = true
state.clearingPaymentInfo = true
}
return state
}
if clear {
var info = BotPaymentInfo()
if values[0] {
info.insert(.paymentInfo)
}
if values[1] {
info.insert(.shippingInfo)
}
clearPaymentInfoDisposable.set((context.engine.payments.clearBotPaymentInfo(info: info)
|> deliverOnMainQueue).start(completed: {
updateState { state in
var state = state
state.clearingPaymentInfo = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String?
if info.contains([.paymentInfo, .shippingInfo]) {
text = presentationData.strings.Privacy_PaymentsClear_AllInfoCleared
} else if info.contains(.paymentInfo) {
text = presentationData.strings.Privacy_PaymentsClear_PaymentInfoCleared
} else if info.contains(.shippingInfo) {
text = presentationData.strings.Privacy_PaymentsClear_ShippingInfoCleared
} else {
text = nil
}
if let text = text {
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }))
}
}))
}
dismissAction()
}))
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller)
}, updateSecretChatLinkPreviews: { value in
let _ = ApplicationSpecificNotice.setSecretChatLinkPreviews(accountManager: context.sharedContext.accountManager, value: value).start()
}, deleteContacts: {
var canBegin = false
updateState { state in
if !state.deletingContacts {
canBegin = true
}
return state
}
if canBegin {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_ContactsResetConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.Common_Delete, action: {
var begin = false
updateState { state in
var state = state
if !state.deletingContacts {
state.deletingContacts = true
begin = true
}
return state
}
if !begin {
return
}
let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: false).start()
actionsDisposable.add((context.engine.contacts.deleteAllContacts()
|> deliverOnMainQueue).start(completed: {
updateState { state in
var state = state
state.deletingContacts = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }))
}))
})]))
}
}, updateSyncContacts: { value in
let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: value).start()
}, updateSuggestFrequentContacts: { value in
let apply: () -> Void = {
updateState { state in
var state = state
state.updatedSuggestFrequentContacts = value
return state
}
let _ = context.engine.peers.updateRecentPeersEnabled(enabled: value).start()
}
if !value {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_TopPeersWarning, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
apply()
})]))
} else {
apply()
}
}, deleteCloudDrafts: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Privacy_DeleteDrafts, color: .destructive, action: {
var clear = false
updateState { state in
var state = state
if !state.deletingCloudDrafts {
clear = true
state.deletingCloudDrafts = true
}
return state
}
if clear {
clearPaymentInfoDisposable.set((context.engine.messages.clearCloudDraftsInteractively()
|> deliverOnMainQueue).start(completed: {
updateState { state in
var state = state
state.deletingCloudDrafts = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }))
}))
}
dismissAction()
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller)
}, openBotListSettings: {
pushControllerImpl?(context.sharedContext.makeBotSettingsScreen(context: context, peerId: nil))
})
actionsDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start())
let hasBotSettings = context.engine.peers.botsWithBiometricState()
|> map { peerIds -> Bool in
return !peerIds.isEmpty
}
|> distinctUntilChanged
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.engine.peers.recentPeers(), hasBotSettings)
|> map { presentationData, state, noticeView, sharedData, preferences, recentPeers, hasBotSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let secretChatLinkPreviews = noticeView.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) })
let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
let synchronizeDeviceContacts: Bool = settings.synchronizeContacts
let suggestRecentPeers: Bool
if let updatedSuggestFrequentContacts = state.updatedSuggestFrequentContacts {
suggestRecentPeers = updatedSuggestFrequentContacts
} else {
switch recentPeers {
case .peers:
suggestRecentPeers = true
case .disabled:
suggestRecentPeers = false
}
}
let rightNavigationButton: ItemListNavigationButton? = nil
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PrivateDataSettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let animateChanges = false
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers, hasBotSettings: hasBotSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller
}