// // CustomListsViewController.swift // Lockdown // // Created by Aliaksandr Dvoineu on 2.06.23. // Copyright © 2023 Confirmed Inc. All rights reserved. // import UIKit import CocoaLumberjackSwift final class CustomListsViewController: UIViewController, DomainListSaveable { // MARK: - Properties var didMakeChange = false var customBlockedDomains: [(String, Bool)] = [] var customBlockedLists: [UserBlockListsGroup] = [] let customBlockedListsTableView = StaticTableView() let customBlockedDomainsTableView = StaticTableView() private lazy var listsSubmenuView: ListsSubmenuView = { let view = ListsSubmenuView() view.topButton.addTarget(self, action: #selector(addList), for: .touchUpInside) view.bottomButton.addTarget(self, action: #selector(importBlockList), for: .touchUpInside) view.isHidden = true return view }() private lazy var addNewListButton: UIButton = { let button = UIButton(type: .system) let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) button.tintColor = .tunnelsBlue button.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) button.addTarget(self, action: #selector(showSubmenu), for: .touchUpInside) button.isEnabled = false return button }() private lazy var listsLabel: UILabel = { let label = UILabel() label.text = NSLocalizedString("Lists", comment: "") label.textColor = .label label.font = fontBold18 return label }() private lazy var emptyListsView: EmptyListsView = { let view = EmptyListsView() view.descriptionLabel.text = NSLocalizedString("No lists yet", comment: "") view.addButton.setTitle(NSLocalizedString("Create a list", comment: ""), for: .normal) view.addButton.addTarget(self, action: #selector(addList), for: .touchUpInside) return view }() private lazy var lockedListsView: LockedListsView = { let view = LockedListsView() return view }() private lazy var domainsLabel: UILabel = { let label = UILabel() label.text = NSLocalizedString("Domains", comment: "") label.textColor = .label label.font = fontBold18 return label }() private lazy var addNewDomainButton: UIButton = { let button = UIButton(type: .system) let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) button.tintColor = .tunnelsBlue button.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) button.addTarget(self, action: #selector(addDomain), for: .touchUpInside) return button }() private lazy var editDomainButton: UIButton = { let button = UIButton(type: .system) let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) button.tintColor = .tunnelsBlue button.setImage(UIImage(named: "icn_edit"), for: .normal) button.addTarget(self, action: #selector(editDomains), for: .touchUpInside) button.isHidden = true return button }() private lazy var emptyDomainsView: EmptyListsView = { let view = EmptyListsView() view.descriptionLabel.text = NSLocalizedString("No custom domains yet", comment: "") view.addButton.setTitle(NSLocalizedString("Add a domain", comment: ""), for: .normal) view.addButton.addTarget(self, action: #selector(addDomain), for: .touchUpInside) return view }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .secondarySystemBackground let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissView)) tap.cancelsTouchesInView = false view.addGestureRecognizer(tap) configureCustomBlockedListsTableView() configureCustomBlockedDomainsTableView() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) reloadCustomBlockedLists() reloadCustomBlockedDomains() } private func configureCustomBlockedListsTableView() { view.addSubview(listsLabel) listsLabel.anchors.top.pin(inset: 12) listsLabel.anchors.leading.marginsPin() view.addSubview(addNewListButton) addNewListButton.anchors.centerY.equal(listsLabel.anchors.centerY) addNewListButton.anchors.trailing.marginsPin() addTableView(customBlockedListsTableView, layout: { tableView in tableView.anchors.top.spacing(0, to: listsLabel.anchors.bottom) tableView.anchors.leading.pin() tableView.anchors.trailing.pin() tableView.anchors.height.equal(250) }) view.addSubview(listsSubmenuView) listsSubmenuView.anchors.trailing.marginsPin() listsSubmenuView.anchors.top.marginsPin() customBlockedListsTableView.deselectsCellsAutomatically = true } private func configureCustomBlockedDomainsTableView() { view.addSubview(domainsLabel) domainsLabel.anchors.top.spacing(290, to: view.anchors.top) domainsLabel.anchors.height.equal(30) domainsLabel.anchors.leading.marginsPin() view.addSubview(addNewDomainButton) addNewDomainButton.anchors.centerY.equal(domainsLabel.anchors.centerY) addNewDomainButton.anchors.trailing.marginsPin() addTableView(customBlockedDomainsTableView, layout: { tableView in tableView.anchors.top.spacing(0, to: domainsLabel.anchors.bottom) tableView.anchors.leading.pin() tableView.anchors.trailing.pin() tableView.anchors.bottom.safeAreaPin() }) customBlockedDomainsTableView.deselectsCellsAutomatically = true reloadCustomBlockedDomains() } } private extension CustomListsViewController { func reloadCustomBlockedLists() { customBlockedListsTableView.clear() customBlockedLists = { let lists = getBlockedLists().userBlockListsDefaults let sorted = lists.sorted(by: { $0.key < $1.key }) return Array(sorted.map(\.value)) }() createCustomBlockedListsRows() customBlockedListsTableView.reloadData() } func reloadCustomBlockedDomains() { customBlockedDomainsTableView.clear() customBlockedDomains = { let domains = getUserBlockedDomains() return domains.sorted(by: { $0.key < $1.key }).map { (key, value) -> (String, Bool) in if let status = value as? NSNumber { return (key, status.boolValue) } else { return (key, false) } } }() createCustomBlockedDomainsRows() customBlockedDomainsTableView.reloadData() } func saveNewList(userEnteredListName: String) { didMakeChange = true DDLogInfo("Adding custom list - \(userEnteredListName)") addBlockedList(listName: userEnteredListName) reloadCustomBlockedLists() } func saveNewDomain(userEnteredDomainName: String) { let validation = DomainNameValidator.validate(userEnteredDomainName) switch validation { case .valid: didMakeChange = true DDLogInfo("Adding custom domain - \(userEnteredDomainName)") addUserBlockedDomain(domain: userEnteredDomainName.lowercased()) reloadCustomBlockedDomains() case .notValid(let reason): DDLogWarn("Custom domain is not valid - \(userEnteredDomainName), reason - \(reason)") // showPopupDialog( // title: NSLocalizedString("Invalid domain", comment: ""), // message: "\"\(userEnteredDomainName)\"" + NSLocalizedString(" is not a valid entry. Please only enter the host of the domain you want to block. For example, \"google.com\" without \"https://\"", comment: ""), // acceptButton: NSLocalizedString("Okay", comment: "") // ) } } func createCustomBlockedListsRows() { let tableView = customBlockedListsTableView let emptyList = emptyListsView let lockedList = lockedListsView if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { addNewListButton.isEnabled = true if customBlockedLists.count == 0 { tableView.addRow { [unowned self] (contentView) in contentView.addSubview(emptyList) emptyListsView.anchors.edges.pin() }.onSelect { [unowned self] in self.addList() } } } else { tableView.addRow { [unowned self] (contentView) in contentView.addSubview(lockedList) lockedList.anchors.edges.pin() }.onSelect { [unowned self] in let vc = VPNPaywallViewController() self.present(vc, animated: true) } } for list in customBlockedLists { let blockListView = BlockListView() blockListView.contents = .listsBlocked(list) let cell = tableView.addRow { [unowned self] (contentView) in contentView.addSubview(blockListView) blockListView.anchors.edges.pin() }.onSelect { [unowned self] in self.didMakeChange = true let vc = ListSettingsViewController() vc.listName = list.name vc.didMakeChange = list.enabled // vc.blockListVC = self navigationController?.pushViewController(vc, animated: true) }.onSwipeToDelete { [unowned self] in self.didMakeChange = true deleteList(list: list.name) DDLogInfo("Deleting custom list - \(list.name)") } cell.accessoryType = .disclosureIndicator } } func createCustomBlockedDomainsRows() { let tableView = customBlockedDomainsTableView let emptyDomains = emptyDomainsView if customBlockedDomains.isEmpty { editDomainButton.isHidden = true tableView.addRow { [unowned self] (contentView) in contentView.addSubview(emptyDomains) emptyDomains.anchors.edges.pin() }.onSelect { [unowned self] in self.addDomain() } } for (domain, isEnabled) in customBlockedDomains { var currentEnabledStatus = isEnabled let blockListView = BlockListView() blockListView.contents = .userBlocked(domain: domain, isEnabled: isEnabled) tableView.addRow { [unowned self] (contentView) in contentView.addSubview(blockListView) blockListView.anchors.edges.pin() self.view.addSubview(editDomainButton) editDomainButton.anchors.centerY.equal(domainsLabel.anchors.centerY) editDomainButton.anchors.trailing.spacing(16, to: addNewDomainButton.anchors.leading) editDomainButton.isHidden = false }.onSelect { [unowned blockListView, unowned self] in self.didMakeChange = true currentEnabledStatus.toggle() blockListView.contents = .userBlocked(domain: domain, isEnabled: currentEnabledStatus) setUserBlockedDomain(domain: domain, enabled: currentEnabledStatus) }.onSwipeToDelete { [unowned self] in self.didMakeChange = true deleteUserBlockedDomain(domain: domain) DDLogInfo("Deleting custom domain - \(domain)") } } } @objc func addList() { let tableView = customBlockedListsTableView showCreateList( initialListName: nil, forDomainList: [] ) { [weak self] _, name in guard let self else { return } self.saveNewList(userEnteredListName: name) self.reloadCustomBlockedLists() self.listsSubmenuView.isHidden = true } } func deleteList(list: String) { let alert = UIAlertController(title: NSLocalizedString("Delete List?", comment: ""), message: NSLocalizedString("Are you sure you want to remove this list?", comment: ""), preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("No, Return", comment: ""), style: .default, handler: { [weak self] (_) in guard let self else { return } self.reloadCustomBlockedLists() })) alert.addAction(UIAlertAction(title: NSLocalizedString("Yes, Delete", comment: ""), style: .destructive, handler: { [weak self] (_) in guard let self else { return } deleteBlockedList(listName: list) self.customBlockedListsTableView.clear() self.reloadCustomBlockedLists() })) present(alert, animated: true, completion: nil) } @objc func showSubmenu() { listsSubmenuView.isHidden = false } @objc func dismissView() { listsSubmenuView.isHidden = true } @objc func importBlockList() { listsSubmenuView.isHidden = true let vc = ImportBlockListViewController() vc.importCompletion = { [unowned self] in self.reloadCustomBlockedLists() self.showSuccessImportAlert() } navigationController?.present(vc, animated: true) } @objc func addDomain() { let tableView = customBlockedDomainsTableView let alertController = UIAlertController(title: NSLocalizedString("Add a Domain to Block", comment: ""), message: nil, preferredStyle: .alert) let saveAction = UIAlertAction(title: "Save", style: .default) { [weak self] (_) in if let txtField = alertController.textFields?.first, let text = txtField.text { guard let self else { return } self.saveNewDomain(userEnteredDomainName: text) if !getUserBlockedDomains().isEmpty { tableView.clear() } self.didMakeChange = true self.reloadCustomBlockedDomains() } } saveAction.isEnabled = false let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (_) in } alertController.addTextField { (textField) in textField.keyboardType = .URL textField.placeholder = "domain-to-block" } NotificationCenter.default.addObserver( forName: UITextField.textDidChangeNotification, object: alertController.textFields?.first, queue: .main) { (notification) -> Void in guard let textFieldText = alertController.textFields?.first?.text else { return } saveAction.isEnabled = textFieldText.isValid(.domainName) && !textFieldText.isEmpty } alertController.addAction(saveAction) alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) } @objc func editDomains() { if !customBlockedDomains.isEmpty { let vc = EditDomainsViewController() vc.updateCompletion = { [weak self] in self?.reloadCustomBlockedDomains() } navigationController?.pushViewController(vc, animated: true) } } func showSuccessImportAlert() { let alert = UIAlertController(title: NSLocalizedString("Success!", comment: ""), message: NSLocalizedString("The list has been imported successfully. You can start blocking the list's domains", comment: ""), preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), style: .default, handler: nil)) present(alert, animated: true, completion: nil) } func showErrorAlert() { let alert = UIAlertController(title: NSLocalizedString("Error", comment: ""), message: NSLocalizedString("Unable to import the list. Please try again or contact support for assistance", comment: ""), preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), style: .default, handler: nil)) present(alert, animated: true, completion: nil) } }