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 init(account: Account, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void, deleteCloudDrafts: @escaping () -> Void) { self.account = account self.clearPaymentInfo = clearPaymentInfo self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews self.deleteContacts = deleteContacts self.updateSyncContacts = updateSyncContacts self.updateSuggestFrequentContacts = updateSuggestFrequentContacts self.deleteCloudDrafts = deleteCloudDrafts } } private enum PrivacyAndSecuritySection: Int32 { case contacts case frequentContacts case chats case payments case secretChats } private enum PrivacyAndSecurityEntry: 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) var section: ItemListSectionId { switch self { case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo: return PrivacyAndSecuritySection.contacts.rawValue case .frequentContacts, .frequentContactsInfo: return PrivacyAndSecuritySection.frequentContacts.rawValue case .chatsHeader, .deleteCloudDrafts: return PrivacyAndSecuritySection.chats.rawValue case .paymentHeader, .clearPaymentInfo, .paymentInfo: return PrivacyAndSecuritySection.payments.rawValue case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo: return PrivacyAndSecuritySection.secretChats.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 } } static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> 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 } } } static func <(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> 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, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteContacts() }) case let .syncContacts(_, text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSyncContacts(updatedValue) }) case let .syncContactsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .frequentContacts(_, text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSuggestFrequentContacts(updatedValue) }) 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, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteCloudDrafts() }) case let .paymentHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .clearPaymentInfo(_, text, enabled): return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.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, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSecretChatLinkPreviews(updatedValue) }) case let .secretChatLinkPreviewsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } 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) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] 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)) return entries } public func dataPrivacyController(context: AccountContext) -> 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)? 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), 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: .destructiveAction, 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.account.postbox.transaction({ transaction in transaction.updatePreferencesEntry(key: PreferencesKeys.contactsSettings, { current in var settings = current?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings settings.synchronizeContacts = false return PreferencesEntry(settings) }) }).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), elevatedLayout: false, action: { _ in return false })) })) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) } }, updateSyncContacts: { value in let _ = context.account.postbox.transaction({ transaction in transaction.updatePreferencesEntry(key: PreferencesKeys.contactsSettings, { current in var settings = current?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings settings.synchronizeContacts = value return PreferencesEntry(settings) }) }).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), elevatedLayout: false, action: { _ in return false })) })) } dismissAction() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller) }) actionsDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start()) 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()) |> map { presentationData, state, noticeView, sharedData, preferences, recentPeers -> (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), style: .blocks, 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)) } return controller }