Telegram-iOS/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift
Ilya Laktyushin 45cc8468bb Various fixes
2026-06-04 19:49:04 +02:00

562 lines
28 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import ItemListUI
import AccountContext
import OpenInExternalAppUI
import ItemListPeerActionItem
import UndoUI
import WebKit
import PersistentStringHash
private final class WebBrowserSettingsControllerArguments {
let context: AccountContext
let updateDefaultBrowser: (String?) -> Void
let clearCookies: () -> Void
let clearCache: () -> Void
let addException: () -> Void
let removeException: (AccountWebBrowserException) -> Void
let clearExceptions: () -> Void
init(
context: AccountContext,
updateDefaultBrowser: @escaping (String?) -> Void,
clearCookies: @escaping () -> Void,
clearCache: @escaping () -> Void,
addException: @escaping () -> Void,
removeException: @escaping (AccountWebBrowserException) -> Void,
clearExceptions: @escaping () -> Void
) {
self.context = context
self.updateDefaultBrowser = updateDefaultBrowser
self.clearCookies = clearCookies
self.clearCache = clearCache
self.addException = addException
self.removeException = removeException
self.clearExceptions = clearExceptions
}
}
private enum WebBrowserSettingsSection: Int32 {
case browsers
case clearCookies
case neverExceptions
case alwaysExceptions
case clear
}
private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
case browserHeader(PresentationTheme, String)
case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32)
case browserInfo(PresentationTheme, String)
case clearCookies(PresentationTheme, String)
case clearCache(PresentationTheme, String)
case clearCookiesInfo(PresentationTheme, String)
case neverHeader(PresentationTheme, String)
case neverAdd(PresentationTheme, String)
case neverException(Int32, PresentationTheme, AccountWebBrowserException)
case neverExceptionsInfo(PresentationTheme, String)
case neverExceptionsClear(PresentationTheme, String)
case alwaysHeader(PresentationTheme, String)
case alwaysAdd(PresentationTheme, String)
case alwaysException(Int32, PresentationTheme, AccountWebBrowserException)
case alwaysExceptionsInfo(PresentationTheme, String)
case alwaysExceptionsClear(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .browserHeader, .browser, .browserInfo:
return WebBrowserSettingsSection.browsers.rawValue
case .clearCookies, .clearCache, .clearCookiesInfo:
return WebBrowserSettingsSection.clearCookies.rawValue
case .neverHeader, .neverAdd, .neverException, .neverExceptionsInfo:
return WebBrowserSettingsSection.neverExceptions.rawValue
case .alwaysHeader, .alwaysAdd, .alwaysException, .alwaysExceptionsInfo:
return WebBrowserSettingsSection.alwaysExceptions.rawValue
case .neverExceptionsClear, .alwaysExceptionsClear:
return WebBrowserSettingsSection.clear.rawValue
}
}
var stableId: UInt64 {
switch self {
case .browserHeader:
return 0
case let .browser(_, _, _, _, _, index):
return UInt64(1 + index)
case .browserInfo:
return 101
case .clearCookies:
return 102
case .clearCache:
return 103
case .clearCookiesInfo:
return 104
case .neverHeader:
return 105
case .neverAdd:
return 106
case let .neverException(_, _, exception):
return 107 + exception.domain.persistentHashValue
case .neverExceptionsInfo:
return 1001
case .neverExceptionsClear:
return 1002
case .alwaysHeader:
return 1003
case .alwaysAdd:
return 1004
case let .alwaysException(_, _, exception):
return 1005 + exception.domain.persistentHashValue
case .alwaysExceptionsInfo:
return 2000
case .alwaysExceptionsClear:
return 3000
}
}
var sortId: Int32 {
switch self {
case .browserHeader:
return 0
case let .browser(_, _, _, _, _, index):
return 1 + index
case .browserInfo:
return 101
case .clearCookies:
return 102
case .clearCache:
return 103
case .clearCookiesInfo:
return 104
case .neverHeader:
return 105
case .neverAdd:
return 106
case let .neverException(index, _, _):
return 107 + index
case .neverExceptionsInfo:
return 1001
case .neverExceptionsClear:
return 1002
case .alwaysHeader:
return 1003
case .alwaysAdd:
return 1004
case let .alwaysException(index, _, _):
return 1005 + index
case .alwaysExceptionsInfo:
return 2000
case .alwaysExceptionsClear:
return 3000
}
}
static func ==(lhs: WebBrowserSettingsControllerEntry, rhs: WebBrowserSettingsControllerEntry) -> Bool {
switch lhs {
case let .browserHeader(lhsTheme, lhsText):
if case let .browserHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .browser(lhsTheme, lhsTitle, lhsApplication, lhsIdentifier, lhsSelected, lhsIndex):
if case let .browser(rhsTheme, rhsTitle, rhsApplication, rhsIdentifier, rhsSelected, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsApplication == rhsApplication, lhsIdentifier == rhsIdentifier, lhsSelected == rhsSelected, lhsIndex == rhsIndex {
return true
} else {
return false
}
case let .browserInfo(lhsTheme, lhsText):
if case let .browserInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .clearCookies(lhsTheme, lhsText):
if case let .clearCookies(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .clearCache(lhsTheme, lhsText):
if case let .clearCache(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .clearCookiesInfo(lhsTheme, lhsText):
if case let .clearCookiesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .neverHeader(lhsTheme, lhsText):
if case let .neverHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .neverException(lhsIndex, lhsTheme, lhsException):
if case let .neverException(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException {
return true
} else {
return false
}
case let .neverAdd(lhsTheme, lhsText):
if case let .neverAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .neverExceptionsInfo(lhsTheme, lhsText):
if case let .neverExceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .neverExceptionsClear(lhsTheme, lhsText):
if case let .neverExceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .alwaysHeader(lhsTheme, lhsText):
if case let .alwaysHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .alwaysException(lhsIndex, lhsTheme, lhsException):
if case let .alwaysException(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException {
return true
} else {
return false
}
case let .alwaysAdd(lhsTheme, lhsText):
if case let .alwaysAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .alwaysExceptionsInfo(lhsTheme, lhsText):
if case let .alwaysExceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .alwaysExceptionsClear(lhsTheme, lhsText):
if case let .alwaysExceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: WebBrowserSettingsControllerEntry, rhs: WebBrowserSettingsControllerEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! WebBrowserSettingsControllerArguments
switch self {
case let .browserHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .browser(_, title, application, identifier, selected, _):
return WebBrowserItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, title: title, application: application, identifier: identifier, checked: selected, sectionId: self.section) {
arguments.updateDefaultBrowser(identifier)
}
case let .browserInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .clearCookies(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.clearCookies()
})
case let .clearCache(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.clearCache()
})
case let .clearCookiesInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .neverHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .neverException(_, _, exception):
return WebBrowserDomainExceptionItem(presentationData: presentationData, systemStyle: .glass, context: arguments.context, title: exception.title, label: exception.domain, favicon: exception.favicon, sectionId: self.section, style: .blocks, deleted: {
arguments.removeException(exception)
})
case let .neverAdd(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.addException()
})
case let .neverExceptionsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .neverExceptionsClear(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.clearExceptions()
})
case let .alwaysHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .alwaysException(_, _, exception):
return WebBrowserDomainExceptionItem(presentationData: presentationData, systemStyle: .glass, context: arguments.context, title: exception.title, label: exception.domain, favicon: exception.favicon, sectionId: self.section, style: .blocks, deleted: {
arguments.removeException(exception)
})
case let .alwaysAdd(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.addException()
})
case let .alwaysExceptionsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .alwaysExceptionsClear(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.clearExceptions()
})
}
}
}
private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, localSettings: WebBrowserSettings, accountSettings: AccountWebBrowserSettings) -> [WebBrowserSettingsControllerEntry] {
var entries: [WebBrowserSettingsControllerEntry] = []
let options = availableOpenInOptions(context: context, item: .url(url: "https://telegram.org"))
let defaultExternalBrowser = localSettings.defaultWebBrowser ?? "default"
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title))
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, !accountSettings.openExternalBrowser, 0))
var index: Int32 = 1
for option in options {
entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, accountSettings.openExternalBrowser && option.identifier == defaultExternalBrowser, index))
index += 1
}
entries.append(.browserInfo(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksInfo))
entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies))
entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info))
//TODO:localize
if accountSettings.openExternalBrowser {
entries.append(.neverHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_OpenInApp))
entries.append(.neverAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
var exceptionIndex: Int32 = 0
for exception in accountSettings.inAppExceptions.reversed() {
entries.append(.neverException(exceptionIndex, presentationData.theme, exception))
exceptionIndex += 1
}
entries.append(.neverExceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_InAppInfo))
if !accountSettings.inAppExceptions.isEmpty {
entries.append(.neverExceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_DeleteAll))
}
} else {
entries.append(.alwaysHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_DontOpenInApp))
entries.append(.alwaysAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
var exceptionIndex: Int32 = 0
for exception in accountSettings.externalExceptions.reversed() {
entries.append(.alwaysException(exceptionIndex, presentationData.theme, exception))
exceptionIndex += 1
}
entries.append(.alwaysExceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info))
if !accountSettings.externalExceptions.isEmpty {
entries.append(.alwaysExceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_DeleteAll))
}
}
return entries
}
public func webBrowserSettingsController(context: AccountContext) -> ViewController {
var clearCookiesImpl: (() -> Void)?
var clearCacheImpl: (() -> Void)?
var addExceptionImpl: (() -> Void)?
var removeExceptionImpl: ((AccountWebBrowserException) -> Void)?
var clearExceptionsImpl: (() -> Void)?
let arguments = WebBrowserSettingsControllerArguments(
context: context,
updateDefaultBrowser: { identifier in
let openExternalBrowser = identifier != nil
if let identifier {
let _ = (updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, {
$0.withUpdatedDefaultWebBrowser(identifier)
})
|> then(updateRemoteWebBrowserSettings(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: openExternalBrowser))).start()
} else {
let _ = updateRemoteWebBrowserSettings(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: openExternalBrowser).start()
}
},
clearCookies: {
clearCookiesImpl?()
},
clearCache: {
clearCacheImpl?()
},
addException: {
addExceptionImpl?()
},
removeException: { exception in
removeExceptionImpl?(exception)
},
clearExceptions: {
clearExceptionsImpl?()
}
)
let previousSettings = Atomic<(WebBrowserSettings, AccountWebBrowserSettings)?>(value: nil)
let signal = combineLatest(
context.sharedContext.presentationData,
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.webBrowserSettings))
)
|> deliverOnMainQueue
|> map { presentationData, sharedData, accountSettingsEntry -> (ItemListControllerState, (ItemListNodeState, Any)) in
let localSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings
let accountSettings = accountSettingsEntry?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings
let previousSettings = previousSettings.swap((localSettings, accountSettings))
var animateChanges = false
if let previousSettings {
if previousSettings.0.defaultWebBrowser != localSettings.defaultWebBrowser || previousSettings.1.openExternalBrowser != accountSettings.openExternalBrowser {
animateChanges = true
}
if previousSettings.1.externalExceptions.count != accountSettings.externalExceptions.count || previousSettings.1.inAppExceptions.count != accountSettings.inAppExceptions.count {
animateChanges = true
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, localSettings: localSettings, accountSettings: accountSettings), style: .blocks, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
clearCookiesImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
updatedPresentationData: nil,
title: nil,
text: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Clear, action: {
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage, WKWebsiteDataTypeSessionStorage], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{})
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(
title: nil,
text: presentationData.strings.WebBrowser_ClearCookies_Succeed,
timeout: nil,
customUndoText: nil
),
elevatedLayout: false,
position: .bottom,
action: { _ in return false }), in: .current
)
})
]
)
controller?.present(alertController, in: .window(.root))
}
clearCacheImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
updatedPresentationData: nil,
title: nil,
text: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Clear, action: {
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{})
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(
title: nil,
text: presentationData.strings.WebBrowser_ClearCache_Succeed,
timeout: nil,
customUndoText: nil
),
elevatedLayout: false,
position: .bottom,
action: { _ in return false }), in: .current
)
})
]
)
controller?.present(alertController, in: .window(.root))
}
addExceptionImpl = { [weak controller] in
var dismissImpl: (() -> Void)?
let linkController = webBrowserDomainController(context: context, apply: { url in
if let url {
let _ = (context.account.postbox.transaction { transaction -> AccountWebBrowserSettings in
return transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings
}
|> mapToSignal { settings -> Signal<Bool, NoError> in
return toggleWebBrowserSettingsException(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: !settings.openExternalBrowser, delete: false, url: url)
}
|> deliverOnMainQueue).startStandalone(next: { _ in
dismissImpl?()
})
}
})
dismissImpl = { [weak linkController] in
linkController?.view.endEditing(true)
linkController?.dismiss(completion: nil)
}
controller?.present(linkController, in: .window(.root))
}
removeExceptionImpl = { exception in
let url = exception.url.isEmpty ? exception.domain : exception.url
let _ = (context.account.postbox.transaction { transaction -> AccountWebBrowserSettings in
return transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings
}
|> mapToSignal { settings -> Signal<Bool, NoError> in
return toggleWebBrowserSettingsException(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: !settings.openExternalBrowser, delete: true, url: url)
}).start()
}
clearExceptionsImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
updatedPresentationData: nil,
title: nil,
text: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Clear, action: {
let _ = deleteWebBrowserSettingsExceptions(postbox: context.account.postbox, network: context.account.network).start()
})
]
)
controller?.present(alertController, in: .window(.root))
}
return controller
}