diff --git a/BraveRewardsUI/Common/PopoverNavigationController.swift b/BraveRewardsUI/Common/PopoverNavigationController.swift index 5c45e6b14f5..99960ee690b 100644 --- a/BraveRewardsUI/Common/PopoverNavigationController.swift +++ b/BraveRewardsUI/Common/PopoverNavigationController.swift @@ -18,13 +18,23 @@ public class PopoverNavigationController: UINavigationController { } } + private class Toolbar: UIToolbar { + override var frame: CGRect { + get { return super.frame.with { $0.size.height = 44 } } + set { super.frame = newValue } + } + override var barPosition: UIBarPosition { + return .bottom + } + } + init() { - super.init(navigationBarClass: NavigationBar.self, toolbarClass: nil) + super.init(navigationBarClass: NavigationBar.self, toolbarClass: Toolbar.self) modalPresentationStyle = .currentContext } public override init(rootViewController: UIViewController) { - super.init(navigationBarClass: NavigationBar.self, toolbarClass: nil) + super.init(navigationBarClass: NavigationBar.self, toolbarClass: Toolbar.self) modalPresentationStyle = .currentContext viewControllers = [rootViewController] } diff --git a/BraveRewardsUI/Common/Tables/TableViewCell.swift b/BraveRewardsUI/Common/Tables/TableViewCell.swift index 2cc491429ed..8005f4aaccd 100644 --- a/BraveRewardsUI/Common/Tables/TableViewCell.swift +++ b/BraveRewardsUI/Common/Tables/TableViewCell.swift @@ -108,7 +108,7 @@ class TableViewCell: UITableViewCell, TableViewReusable { if accessoryType == .none { $0.trailing.equalToSuperview().inset(layoutMargins.right) } else { - $0.trailing.equalToSuperview() + $0.trailing.equalToSuperview().inset(6) } $0.centerY.equalToSuperview() } diff --git a/BraveRewardsUI/Extensions/BraveLedgerExtensions.swift b/BraveRewardsUI/Extensions/BraveLedgerExtensions.swift index 2687c062a32..875478b5be8 100644 --- a/BraveRewardsUI/Extensions/BraveLedgerExtensions.swift +++ b/BraveRewardsUI/Extensions/BraveLedgerExtensions.swift @@ -11,6 +11,35 @@ private let log = Logger.rewardsLogger extension BraveLedger { + // MARK: - Auto-Contribute Publisher Helpers + + var supportedPublishersFilter: ActivityInfoFilter { + let sort = ActivityInfoFilterOrderPair().then { + $0.propertyName = "percent" + $0.ascending = false + } + let filter = ActivityInfoFilter().then { + $0.id = "" + $0.excluded = .filterAllExceptExcluded + $0.percent = 1 //exclude 0% sites. + $0.orderBy = [sort] + $0.nonVerified = self.allowUnverifiedPublishers + $0.reconcileStamp = self.autoContributeProps.reconcileStamp + } + return filter + } + + var excludedPublishersFilter: ActivityInfoFilter { + return ActivityInfoFilter().then { + $0.id = "" + $0.excluded = .filterExcluded + $0.nonVerified = self.allowUnverifiedPublishers + $0.reconcileStamp = self.autoContributeProps.reconcileStamp + } + } + + // MARK: - External Wallet Helpers + var walletBalances: [WalletType: Double] { guard let wallets = balance?.wallets else { return [:] } var balances: [WalletType: Double] = [:] @@ -35,6 +64,8 @@ extension BraveLedger { return balance?.total ?? 0 } + // MARK: - Balance and Currency Helpers + /// Get the current BAT wallet balance for display var balanceString: String { return BATValue(balanceTotal).displayString } diff --git a/BraveRewardsUI/Localized Strings/Strings.swift b/BraveRewardsUI/Localized Strings/Strings.swift index f28c39622ec..69a7593600d 100644 --- a/BraveRewardsUI/Localized Strings/Strings.swift +++ b/BraveRewardsUI/Localized Strings/Strings.swift @@ -290,5 +290,9 @@ internal extension Strings { static let userWalletNotificationWalletDisconnectedBody = NSLocalizedString("UserWalletNotificationWalletDisconnectedBody", bundle: .rewardsUI, value: "No worries. This can happen for a variety of security reasons. Reconnecting your wallet will solve this issue.", comment: "The message you receive when your user wallet is unreachable/disconnected") static let userWalletNotificationNowVerifiedTitle = NSLocalizedString("UserWalletNotificationNowVerifiedTitle", bundle: .rewardsUI, value: "Your wallet is verified!", comment: "The message you receive when your user wallet has been verified") static let userWalletNotificationNowVerifiedBody = NSLocalizedString("UserWalletNotificationNowVerifiedBody", bundle: .rewardsUI, value: "Congrats! Your %@ wallet was successfully verified and ready to add and withdraw funds.", comment: "The message you receive when your user wallet has been verified (%@ = The user wallet name, such as \"Uphold\")") + static let exclusionListTitle = NSLocalizedString("ExclusionListTitle", bundle: .rewardsUI, value: "Excluded sites", comment: "The title of the screen that shows a list of excluded sites for Auto-Contribute") + static let restoreAllSitesToolbarButtonTitle = NSLocalizedString("ExclusionListRestoreAllButton", bundle: .rewardsUI, value: "Restore All Sites", comment: "The button that restores all excluded publishers while on the Auto-Contribute excluson list") + static let restore = NSLocalizedString("ExclusionListRestore", bundle: .rewardsUI, value: "Restore", comment: "The swipe-to-delete title when restoring a single item in the Auto-Contribute exclusion list") + static let emptyExclusionList = NSLocalizedString("EmptyExclusionList", bundle: .rewardsUI, value: "No publishers excluded", comment: "The copy the user sees when they are viewing an empty Auto-Contribute exclusion list.") } diff --git a/BraveRewardsUI/RewardsPanelController.swift b/BraveRewardsUI/RewardsPanelController.swift index f273d033830..c5a516bc735 100644 --- a/BraveRewardsUI/RewardsPanelController.swift +++ b/BraveRewardsUI/RewardsPanelController.swift @@ -28,6 +28,9 @@ public class RewardsPanelController: PopoverNavigationController { navigationBar.tintColor = Colors.blurple400 navigationBar.titleTextAttributes = [.foregroundColor: UIColor.black] + toolbar.appearanceBarTintColor = toolbar.barTintColor + toolbar.tintColor = Colors.blurple400 + if #available(iOS 13.0, *) { overrideUserInterfaceStyle = .light } diff --git a/BraveRewardsUI/Settings/Auto-Contribute/Details/AutoContributeDetailsViewController.swift b/BraveRewardsUI/Settings/Auto-Contribute/Details/AutoContributeDetailsViewController.swift index 333ed0b6da6..040f28f0a75 100644 --- a/BraveRewardsUI/Settings/Auto-Contribute/Details/AutoContributeDetailsViewController.swift +++ b/BraveRewardsUI/Settings/Auto-Contribute/Details/AutoContributeDetailsViewController.swift @@ -12,9 +12,8 @@ class AutoContributeDetailViewController: UIViewController { } // Just copy pasted this in, needs design specific for auto-contribute - private static let pageSize = 10 - private var publishers: [PublisherInfo] = [] - private var hasMoreContent = true + private var publishersCount: UInt = 0 + private var excludedPublishersCount: UInt = 0 private let state: RewardsState init(state: RewardsState) { @@ -25,6 +24,15 @@ class AutoContributeDetailViewController: UIViewController { setupLedgerObservers() } + func setupLedgerObservers() { + ledgerObserver.excludedSitesChanged = { _, _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + // TODO: Remove this delay after DB migration + self.reloadData() + } + } + } + @available(*, unavailable) required init(coder: NSCoder) { fatalError() @@ -40,137 +48,107 @@ class AutoContributeDetailViewController: UIViewController { contentView.tableView.dataSource = self title = Strings.autoContribute - - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(tappedEditButton)) - } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + reloadData() } - func reloadData() { - loadPublishers(start: 0) {[weak self] publishersList in - guard let self = self else { return } - self.publishers = publishersList - self.hasMoreContent = publishersList.count == AutoContributeDetailViewController.pageSize - self.contentView.tableView.reloadData() - if !self.contentView.tableView.isEditing { - self.navigationItem.rightBarButtonItem?.isEnabled = !self.publishers.isEmpty - } + private var nextContributionDateView: LabelAccessoryView { + let view = LabelAccessoryView() + let dateFormatter = DateFormatter().then { + $0.dateFormat = Strings.autoContributeDateFormat } + let reconcileDate = Date(timeIntervalSince1970: TimeInterval(state.ledger.autoContributeProps.reconcileStamp)) + view.label.text = dateFormatter.string(from: reconcileDate) + view.bounds = CGRect(origin: .zero, size: view.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize)) + return view } - private func loadPublishers(start: Int, limit: Int = AutoContributeDetailViewController.pageSize, completion: @escaping ([PublisherInfo]) -> Void) { - let sort = ActivityInfoFilterOrderPair().then { - $0.propertyName = "percent" - $0.ascending = false - } - let filter = ActivityInfoFilter().then { - $0.id = "" - $0.excluded = .filterAllExceptExcluded - $0.percent = 1 //exclude 0% sites. - $0.orderBy = [sort] - $0.nonVerified = state.ledger.allowUnverifiedPublishers - $0.reconcileStamp = state.ledger.autoContributeProps.reconcileStamp - } - - state.ledger.listActivityInfo(fromStart: UInt32(start), limit: UInt32(limit), filter: filter) { publishersList in - completion(publishersList) + private func reloadData() { + let filter = state.ledger.supportedPublishersFilter + state.ledger.listActivityInfo(fromStart: 0, limit: 0, filter: filter) { pubs in + self.publishersCount = UInt(pubs.count) + self.contentView.tableView.reloadData() } + excludedPublishersCount = state.ledger.numberOfExcludedPublishers + contentView.tableView.reloadData() } - - private func totalSitesAttributedString(from total: Int) -> NSAttributedString { - let format = String(format: Strings.totalSites, total) - let s = NSMutableAttributedString(string: format) - guard let range = format.range(of: String(total)) else { return s } - s.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0, weight: .semibold), range: NSRange(range, in: format)) - return s +} + +extension AutoContributeDetailViewController: UITableViewDataSource, UITableViewDelegate { + private enum Section: Int, CaseIterable { + case summary + case sites + case settings } - private let headerView = TableHeaderRowView( - columns: [ - TableHeaderRowView.Column( - title: Strings.site.uppercased(), - width: .percentage(0.7) - ), - TableHeaderRowView.Column( - title: Strings.attention.uppercased(), - width: .percentage(0.3), - align: .right - ), - ], - tintColor: BraveUX.autoContributeTintColor - ) - - enum SummaryRows: Int, CaseIterable { - case settings + private enum SummaryRows: Int, CaseIterable { case monthlyPayment case nextContribution - case supportedSites - case excludedSites func dequeuedCell(from tableView: UITableView, indexPath: IndexPath) -> TableViewCell { switch self { - case .monthlyPayment, .supportedSites: + case .monthlyPayment: return tableView.dequeueReusableCell(for: indexPath) as Value1TableViewCell - case .nextContribution, .settings, .excludedSites: + case .nextContribution: return tableView.dequeueReusableCell(for: indexPath) as TableViewCell } } + } + + private enum SitesRows: Int, CaseIterable { + case supportedSites + case excludedSites - static func numberOfRows(_ isExcludingSites: Bool) -> Int { - var cases = Set(SummaryRows.allCases) - if !isExcludingSites { - cases.remove(.excludedSites) - } - return cases.count + func dequeuedCell(from tableView: UITableView, indexPath: IndexPath) -> TableViewCell { + return tableView.dequeueReusableCell(for: indexPath) as Value1TableViewCell } } - private var nextContributionDateView: LabelAccessoryView { - let view = LabelAccessoryView() - let dateFormatter = DateFormatter().then { - $0.dateFormat = Strings.autoContributeDateFormat + private enum SettingsRows: Int, CaseIterable { + case minimumLength + case minimumVisits + case allowUnverifiedContributions + case allowVideoContributions + + func dequeuedCell(from tableView: UITableView, indexPath: IndexPath) -> TableViewCell { + switch self { + case .minimumLength, .minimumVisits: + return tableView.dequeueReusableCell(for: indexPath) as Value1TableViewCell + default: + return tableView.dequeueReusableCell(for: indexPath) as TableViewCell + } + } + var accessoryType: UITableViewCell.AccessoryType { + switch self { + case .minimumLength, .minimumVisits: + return .disclosureIndicator + default: + return .none + } } - let reconcileDate = Date(timeIntervalSince1970: TimeInterval(state.ledger.autoContributeProps.reconcileStamp)) - view.label.text = dateFormatter.string(from: reconcileDate) - view.bounds = CGRect(origin: .zero, size: view.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize)) - return view } - // MARK: - Actions - - @objc private func tappedEditButton() { - contentView.tableView.setEditing(true, animated: true) - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(tappedDoneButton)) + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return CGFloat.leastNormalMagnitude } - @objc private func tappedDoneButton() { - contentView.tableView.setEditing(false, animated: true) - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(tappedEditButton)) - navigationItem.rightBarButtonItem?.isEnabled = !self.publishers.isEmpty - } -} - -extension AutoContributeDetailViewController: UITableViewDataSource, UITableViewDelegate { - private enum Section: Int, CaseIterable { - case summary - case contributions + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return UIView() } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - switch indexPath.section { - case Section.summary.rawValue: - switch indexPath.row { - case SummaryRows.settings.rawValue: - // Settings - let controller = AutoContributeSettingsViewController(ledger: state.ledger) - navigationController?.pushViewController(controller, animated: true) - case SummaryRows.monthlyPayment.rawValue: + guard let section = Section(rawValue: indexPath.section) else { return } + switch section { + case .summary: + guard let row = SummaryRows(rawValue: indexPath.row) else { return } + switch row { + case .monthlyPayment: // Monthly payment guard let wallet = state.ledger.walletInfo else { break } let monthlyPayment = state.ledger.contributionAmount @@ -191,29 +169,53 @@ extension AutoContributeDetailViewController: UITableViewDataSource, UITableView } controller.title = Strings.autoContributeMonthlyPaymentTitle navigationController?.pushViewController(controller, animated: true) - case SummaryRows.excludedSites.rawValue: - let numberOfExcludedSites = state.ledger.numberOfExcludedPublishers - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - if let presenter = alert.popoverPresentationController, let cell = tableView.cellForRow(at: indexPath) { - presenter.sourceView = cell - presenter.sourceRect = cell.bounds - presenter.permittedArrowDirections = [.up, .down] - } - alert.addAction(UIAlertAction(title: String(format: Strings.autoContributeRestoreExcludedSites, numberOfExcludedSites), style: .default, handler: { _ in - self.state.ledger.restoreAllExcludedPublishers() - })) - alert.addAction(UIAlertAction(title: Strings.cancel, style: .cancel, handler: nil)) - present(alert, animated: true) default: break } - case Section.contributions.rawValue: - if !publishers.isEmpty, let url = URL(string: publishers[indexPath.row].url) { - state.delegate?.loadNewTabWithURL(url) + case .sites: + guard let row = SitesRows(rawValue: indexPath.row) else { return } + switch row { + case .supportedSites: + let supportedList = AutoContributeSupportedListController(state: state) + navigationController?.pushViewController(supportedList, animated: true) + case .excludedSites: + let exclusionList = AutoContributeExclusionListController(state: state) + navigationController?.pushViewController(exclusionList, animated: true) + } + case .settings: + guard let row = SettingsRows(rawValue: indexPath.row) else { return } + switch row { + case .minimumLength: + let choices = BraveLedger.MinimumVisitDurationOptions.allCases.map { $0.rawValue } + let selectedIndex = choices.firstIndex(of: state.ledger.minimumVisitDuration) ?? 0 + let controller = OptionsSelectionViewController( + options: BraveLedger.MinimumVisitDurationOptions.allCases, + selectedOptionIndex: selectedIndex) { [weak self] (selectedIndex) in + guard let self = self else { return } + if selectedIndex < choices.count { + self.state.ledger.minimumVisitDuration = choices[selectedIndex] + } + self.navigationController?.popViewController(animated: true) + } + controller.title = Strings.autoContributeMinimumLength + navigationController?.pushViewController(controller, animated: true) + case .minimumVisits: + let choices = BraveLedger.MinimumVisitsOptions.allCases.map { $0.rawValue } + let selectedIndex = choices.firstIndex(of: state.ledger.minimumNumberOfVisits) ?? 0 + let controller = OptionsSelectionViewController( + options: BraveLedger.MinimumVisitsOptions.allCases, + selectedOptionIndex: selectedIndex) { [weak self] (selectedIndex) in + guard let self = self else { return } + if selectedIndex < choices.count { + self.state.ledger.minimumNumberOfVisits = choices[selectedIndex] + } + self.navigationController?.popViewController(animated: true) + } + controller.title = Strings.autoContributeMinimumVisits + navigationController?.pushViewController(controller, animated: true) + default: + break } - - default: - break } } @@ -221,49 +223,15 @@ extension AutoContributeDetailViewController: UITableViewDataSource, UITableView return Section.allCases.count } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let typedSection = Section(rawValue: section), typedSection == .contributions else { return nil } - return headerView - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - guard let typedSection = Section(rawValue: section), typedSection == .contributions else { return 0.0 } - return headerView.systemLayoutSizeFitting( - CGSize(width: tableView.bounds.width, height: tableView.bounds.height), - withHorizontalFittingPriority: .required, - verticalFittingPriority: .fittingSizeLevel - ).height - } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let typedSection = Section(rawValue: section) else { return 0 } switch typedSection { case .summary: - let isExcludingSites = state.ledger.numberOfExcludedPublishers > 0 - return SummaryRows.numberOfRows(isExcludingSites) - case .contributions: - return publishers.isEmpty ? 1 : publishers.count - } - } - - func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - guard Section(rawValue: indexPath.section) == .contributions, !publishers.isEmpty else { return false } - return true - } - - func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { - guard Section(rawValue: indexPath.section) == .contributions, !publishers.isEmpty else { return .none } - return .delete - } - - func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { - return Strings.exclude - } - - func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - guard Section(rawValue: indexPath.section) == .contributions else { return } - if let publisher = publishers[safe: indexPath.row] { - state.ledger.updatePublisherExclusionState(withId: publisher.id, state: .excluded) + return SummaryRows.allCases.count + case .sites: + return SitesRows.allCases.count + case .settings: + return SettingsRows.allCases.count } } @@ -285,12 +253,6 @@ extension AutoContributeDetailViewController: UITableViewDataSource, UITableView cell.accessoryType = .none cell.selectionStyle = .none switch row { - case .settings: - cell.label.text = Strings.settings - cell.imageView?.image = UIImage(frameworkResourceNamed: "settings").alwaysTemplate - cell.imageView?.tintColor = BraveUX.autoContributeTintColor - cell.accessoryType = .disclosureIndicator - cell.selectionStyle = .default case .monthlyPayment: cell.label.text = Strings.autoContributeMonthlyPayment cell.accessoryType = .disclosureIndicator @@ -302,66 +264,69 @@ extension AutoContributeDetailViewController: UITableViewDataSource, UITableView case .nextContribution: cell.label.text = Strings.autoContributeNextDate cell.accessoryView = nextContributionDateView + } + return cell + case .sites: + guard let row = SitesRows(rawValue: indexPath.row) else { return UITableViewCell() } + let cell = row.dequeuedCell(from: tableView, indexPath: indexPath) + cell.manualSeparators = [] + cell.label.font = SettingsUX.bodyFont + cell.label.appearanceTextColor = .black + cell.label.numberOfLines = 0 + cell.accessoryLabel?.appearanceTextColor = Colors.grey100 + cell.accessoryLabel?.font = UIFont.monospacedDigitSystemFont(ofSize: 14.0, weight: .semibold) + cell.accessoryType = .disclosureIndicator + cell.selectionStyle = .default + switch row { case .supportedSites: cell.label.text = Strings.autoContributeSupportedSites - cell.accessoryLabel?.attributedText = totalSitesAttributedString(from: publishers.count) + cell.accessoryLabel?.text = "\(publishersCount)" case .excludedSites: - let numberOfExcludedSites = state.ledger.numberOfExcludedPublishers - cell.label.text = String(format: Strings.autoContributeRestoreExcludedSites, numberOfExcludedSites) - cell.label.appearanceTextColor = Colors.blurple400 - cell.selectionStyle = .default + cell.label.text = Strings.exclusionListTitle + cell.accessoryLabel?.text = "\(excludedPublishersCount)" } return cell - case .contributions: - if publishers.isEmpty { - let cell = tableView.dequeueReusableCell(for: indexPath) as EmptyTableCell - cell.label.text = Strings.emptyAutoContribution - return cell - } - guard let publisher = publishers[safe: indexPath.row] else { - assertionFailure("No Publisher found at index: \(indexPath.row)") - return UITableViewCell() - } - let cell = tableView.dequeueReusableCell(for: indexPath) as AutoContributeCell - cell.selectionStyle = .none - - if let url = URL(string: publisher.url) { - state.dataSource?.retrieveFavicon(for: url, faviconURL: URL(string: publisher.faviconUrl)) { data in - cell.siteImageView.image = data?.image ?? UIImage(frameworkResourceNamed: "defaultFavicon") - cell.siteImageView.backgroundColor = data?.backgroundColor - } - } - - cell.verifiedStatusImageView.isHidden = publisher.status == .notVerified - let provider = " \(publisher.provider.isEmpty ? "" : String(format: Strings.onProviderText, publisher.providerDisplayString))" - let attrName = NSMutableAttributedString(string: publisher.name).then { - $0.append(NSMutableAttributedString(string: provider, attributes: [.font: UIFont.boldSystemFont(ofSize: 14.0), - .foregroundColor: UIColor.gray])) + case .settings: + guard let row = SettingsRows(rawValue: indexPath.row) else { return UITableViewCell() } + let cell = row.dequeuedCell(from: tableView, indexPath: indexPath) + cell.manualSeparators = [] + cell.label.font = SettingsUX.bodyFont + cell.label.numberOfLines = 0 + cell.label.lineBreakMode = .byWordWrapping + cell.accessoryLabel?.appearanceTextColor = Colors.grey100 + cell.accessoryLabel?.font = SettingsUX.bodyFont + cell.accessoryType = row.accessoryType + switch row { + case .minimumLength: + cell.label.text = Strings.autoContributeMinimumLengthMessage + cell.accessoryLabel?.text = BraveLedger.MinimumVisitDurationOptions(rawValue: state.ledger.minimumVisitDuration)?.displayString + case .minimumVisits: + cell.label.text = Strings.autoContributeMinimumVisitsMessage + cell.accessoryLabel?.text = BraveLedger.MinimumVisitsOptions(rawValue: state.ledger.minimumNumberOfVisits)?.displayString + case .allowUnverifiedContributions: + cell.label.text = Strings.autoContributeToUnverifiedSites + cell.accessoryView = contentView.allowUnverifiedContributionsSwitch + cell.selectionStyle = .none + case .allowVideoContributions: + cell.label.text = Strings.autoContributeToVideos + cell.accessoryView = contentView.allowVideoContributionsSwitch + cell.selectionStyle = .none } - cell.siteNameLabel.attributedText = attrName - cell.attentionAmount = CGFloat(publisher.percent) return cell } } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.section == Section.contributions.rawValue, - hasMoreContent && indexPath.row == publishers.count - 2 { - - loadPublishers(start: publishers.count) {[weak self] publisherList in - guard let self = self else { return } - self.publishers.append(contentsOf: publisherList) - self.hasMoreContent = publisherList.count == AutoContributeDetailViewController.pageSize - // TODO: Animate this update - tableView.reloadData() - } - } - } } extension AutoContributeDetailViewController { class View: UIView { let tableView = UITableView(frame: .zero, style: .grouped) + let allowUnverifiedContributionsSwitch = UISwitch().then { + $0.onTintColor = BraveUX.braveOrange + } + + let allowVideoContributionsSwitch = UISwitch().then { + $0.onTintColor = BraveUX.braveOrange + } override init(frame: CGRect) { super.init(frame: frame) @@ -371,7 +336,6 @@ extension AutoContributeDetailViewController { } tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) tableView.separatorInset = .zero - tableView.register(AutoContributeCell.self) tableView.register(TableViewCell.self) tableView.register(Value1TableViewCell.self) tableView.register(EmptyTableCell.self) @@ -390,29 +354,3 @@ extension AutoContributeDetailViewController { } } } - -/// Ledger Observers -extension AutoContributeDetailViewController { - func setupLedgerObservers() { - ledgerObserver.excludedSitesChanged = { [weak self] key, exclude -> Void in - guard let self = self, self.isViewLoaded else { - return - } - let tableView = self.contentView.tableView - switch exclude { - case .all: - tableView.reloadData() - case .excluded: - //The delay is to ensure the db is updated. This is just a fail safe. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { - self.loadPublishers(start: 0, limit: self.publishers.count, completion: { info in - self.publishers = info - tableView.reloadData() - }) - }) - default: - return - } - } - } -} diff --git a/BraveRewardsUI/Settings/Auto-Contribute/Exclusion List/AutoContributeExclusionListController.swift b/BraveRewardsUI/Settings/Auto-Contribute/Exclusion List/AutoContributeExclusionListController.swift new file mode 100644 index 00000000000..ba17eae6c9f --- /dev/null +++ b/BraveRewardsUI/Settings/Auto-Contribute/Exclusion List/AutoContributeExclusionListController.swift @@ -0,0 +1,236 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import BraveRewards + +final class AutoContributeExclusionListController: UIViewController { + private let state: RewardsState + private let ledgerObserver: LedgerObserver + + init(state: RewardsState) { + self.state = state + ledgerObserver = LedgerObserver(ledger: state.ledger) + state.ledger.add(ledgerObserver) + super.init(nibName: nil, bundle: nil) + setupLedgerObservers() + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + + private var contentView: View { + return view as! View // swiftlint:disable:this force_cast + } + + override func loadView() { + view = View() + } + + private lazy var editButton = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(tappedEdit)) + private lazy var doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(tappedDone)) + private lazy var restoreAllButton = UIBarButtonItem(title: Strings.restoreAllSitesToolbarButtonTitle, style: .plain, target: self, action: #selector(tappedRestoreAll(_:))) + + override func viewDidLoad() { + super.viewDidLoad() + contentView.tableView.delegate = self + contentView.tableView.dataSource = self + + title = Strings.exclusionListTitle.capitalized + + navigationItem.rightBarButtonItem = editButton + toolbarItems = [ + .init(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + restoreAllButton, + .init(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + ] + + // Not enabled unless we have more than 1 publisher + restoreAllButton.isEnabled = false + reloadData() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setToolbarHidden(false, animated: animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.setToolbarHidden(true, animated: animated) + } + + @objc private func tappedEdit() { + contentView.tableView.setEditing(true, animated: true) + navigationItem.setRightBarButton(doneButton, animated: true) + } + + @objc private func tappedDone() { + contentView.tableView.setEditing(false, animated: true) + navigationItem.setRightBarButton(editButton, animated: true) + } + + @objc private func tappedRestoreAll(_ sender: UIBarButtonItem) { + let numberOfExcludedSites = state.ledger.numberOfExcludedPublishers + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let presenter = alert.popoverPresentationController { + presenter.barButtonItem = sender + presenter.permittedArrowDirections = [.up, .down] + } + alert.addAction(UIAlertAction(title: String(format: Strings.autoContributeRestoreExcludedSites, numberOfExcludedSites), style: .default, handler: { _ in + self.state.ledger.restoreAllExcludedPublishers() + self.navigationController?.popViewController(animated: true) + })) + alert.addAction(UIAlertAction(title: Strings.cancel, style: .cancel, handler: nil)) + present(alert, animated: true) + } + + // MARK: - Data + + private var publishers: [PublisherInfo] = [] { + didSet { + restoreAllButton.isEnabled = !publishers.isEmpty + } + } + + func reloadData() { + let filter = state.ledger.excludedPublishersFilter + state.ledger.listActivityInfo(fromStart: 0, limit: 0, filter: filter) { [weak self] list in + guard let self = self else { return } + self.publishers = list + self.editButton.isEnabled = !list.isEmpty + self.contentView.tableView.reloadData() + } + } + + func setupLedgerObservers() { + ledgerObserver.excludedSitesChanged = { [weak self] _, _ in + guard let self = self, self.isViewLoaded else { + return + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { + self.reloadData() + }) + } + } +} + +// MARK: - UITableViewDelegate +extension AutoContributeExclusionListController: UITableViewDelegate { + func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { + navigationItem.setRightBarButton(doneButton, animated: true) + } + + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + navigationItem.setRightBarButton(editButton, animated: true) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if !publishers.isEmpty, let url = URL(string: publishers[indexPath.row].url) { + state.delegate?.loadNewTabWithURL(url) + } + } +} + +// MARK: - UITableViewDataSource +extension AutoContributeExclusionListController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return publishers.isEmpty ? 1 : publishers.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if publishers.isEmpty { + let cell = tableView.dequeueReusableCell(for: indexPath) as EmptyTableCell + cell.label.text = Strings.emptyExclusionList + return cell + } + guard let publisher = publishers[safe: indexPath.row] else { + assertionFailure("No Publisher found at index: \(indexPath.row)") + return UITableViewCell() + } + let cell = tableView.dequeueReusableCell(for: indexPath) as ExclusionListCell + if let url = URL(string: publisher.url) { + state.dataSource?.retrieveFavicon(for: url, faviconURL: URL(string: publisher.faviconUrl)) { data in + cell.siteImageView.image = data?.image ?? UIImage(frameworkResourceNamed: "defaultFavicon") + cell.siteImageView.backgroundColor = data?.backgroundColor + } + } + + cell.verifiedStatusImageView.isHidden = publisher.status == .notVerified + let provider = " \(publisher.provider.isEmpty ? "" : String(format: Strings.onProviderText, publisher.providerDisplayString))" + let attrName = NSMutableAttributedString(string: publisher.name).then { + $0.append(NSMutableAttributedString(string: provider, attributes: [.font: UIFont.boldSystemFont(ofSize: 14.0), + .foregroundColor: UIColor.gray])) + } + cell.siteNameLabel.attributedText = attrName + return cell + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let action = UIContextualAction( + style: .normal, + title: Strings.restore, + handler: { action, view, handler in + self.tableView(tableView, commit: .delete, forRowAt: indexPath) + handler(true) + } + ) + action.backgroundColor = Colors.blurple400 + let config = UISwipeActionsConfiguration( + actions: [ + action + ] + ) + return config + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if let publisher = publishers[safe: indexPath.row] { + tableView.performBatchUpdates({ + publishers.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .automatic) + if publishers.isEmpty { + tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) + } + }, completion: { _ in + self.state.ledger.updatePublisherExclusionState( + withId: publisher.id, + state: .default + ) + }) + } + } +} + +extension AutoContributeExclusionListController { + private class View: UIView { + let tableView = UITableView(frame: .zero, style: .grouped) + + override init(frame: CGRect) { + super.init(frame: frame) + + tableView.backgroundView = UIView().then { + $0.backgroundColor = SettingsUX.backgroundColor + } + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) + tableView.separatorInset = .zero + tableView.register(ExclusionListCell.self) + tableView.register(EmptyTableCell.self) + tableView.layoutMargins = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + tableView.appearanceSeparatorColor = UIColor(white: 0.85, alpha: 1.0) + + addSubview(tableView) + tableView.snp.makeConstraints { + $0.edges.equalTo(self) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + } +} diff --git a/BraveRewardsUI/Settings/Auto-Contribute/Exclusion List/ExclusionListCell.swift b/BraveRewardsUI/Settings/Auto-Contribute/Exclusion List/ExclusionListCell.swift new file mode 100644 index 00000000000..0d7a3ff37a5 --- /dev/null +++ b/BraveRewardsUI/Settings/Auto-Contribute/Exclusion List/ExclusionListCell.swift @@ -0,0 +1,79 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit + +class ExclusionListCell: UITableViewCell, TableViewReusable { + + private let siteStackView = UIStackView() + + let siteImageView = PublisherIconCircleImageView(size: 28) + + let verifiedStatusImageView = UIImageView(image: UIImage(frameworkResourceNamed: "icn-verify")).then { + $0.isHidden = true + } + let siteNameLabel = UILabel().then { + $0.appearanceTextColor = SettingsUX.bodyTextColor + $0.font = SettingsUX.bodyFont + $0.setContentCompressionResistancePriority(.required, for: .horizontal) + $0.numberOfLines = 0 + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: nil) + + siteStackView.spacing = verifiedStatusImageView.image?.size.width ?? 10.0 + + backgroundColor = .white + + contentView.addSubview(siteStackView) + contentView.addSubview(verifiedStatusImageView) + siteStackView.addArrangedSubview(siteImageView) + siteStackView.addArrangedSubview(siteNameLabel) + + siteStackView.snp.makeConstraints { + $0.top.bottom.equalTo(contentView).inset(10.0) + $0.leading.trailing.equalTo(contentView).inset(15.0) + } + verifiedStatusImageView.snp.makeConstraints { + $0.top.equalTo(siteStackView).offset(-4.0) + $0.leading.equalTo(siteImageView.snp.trailing).offset(-8.0) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + UIColor(white: 0.85, alpha: 1.0).setFill() + let height = 1.0 / UIScreen.main.scale + UIRectFill(CGRect(x: 0, y: rect.maxY - height, width: rect.width, height: height)) + } + + // MARK: - Unavailable + + // swiftlint:disable unused_setter_value + @available(*, unavailable) + override var textLabel: UILabel? { + get { return super.textLabel } + set { } + } + + @available(*, unavailable) + override var detailTextLabel: UILabel? { + get { return super.detailTextLabel } + set { } + } + + @available(*, unavailable) + override var imageView: UIImageView? { + get { return super.imageView } + set { } + } + // swiftlint:enable unused_setter_value +} diff --git a/BraveRewardsUI/Settings/Auto-Contribute/Settings/AutoContributeSettingsViewController.swift b/BraveRewardsUI/Settings/Auto-Contribute/Settings/AutoContributeSettingsViewController.swift deleted file mode 100644 index c128b547b6d..00000000000 --- a/BraveRewardsUI/Settings/Auto-Contribute/Settings/AutoContributeSettingsViewController.swift +++ /dev/null @@ -1,223 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import UIKit -import BraveRewards - -class AutoContributeSettingsViewController: UIViewController { - - private let ledger: BraveLedger - - init(ledger: BraveLedger) { - self.ledger = ledger - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init(coder: NSCoder) { - fatalError() - } - - var contentView: View { - return view as! View // swiftlint:disable:this force_cast - } - - override func loadView() { - self.view = View() - } - - override func viewDidLoad() { - super.viewDidLoad() - - contentView.tableView.delegate = self - contentView.tableView.dataSource = self - - contentView.allowVideoContributionsSwitch.addTarget(self, action: #selector(allowVideoValueChanged), for: .valueChanged) - contentView.allowUnverifiedContributionsSwitch.addTarget(self, action: #selector(allowUnverifiedValueChanged), for: .valueChanged) - - reloadData() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - reloadData() - } - - func reloadData() { - contentView.allowUnverifiedContributionsSwitch.isOn = ledger.allowUnverifiedPublishers - contentView.allowVideoContributionsSwitch.isOn = ledger.allowVideoContributions - - contentView.tableView.reloadData() - } - - enum Row: Int, CaseIterable { - case monthlyPayment - case minimumLength - case minimumVisits - case allowUnverifiedContributions - case allowVideoContributions - - func dequeuedCell(from tableView: UITableView, indexPath: IndexPath) -> TableViewCell { - switch self { - case .monthlyPayment, .minimumLength, .minimumVisits: - return tableView.dequeueReusableCell(for: indexPath) as Value1TableViewCell - default: - return tableView.dequeueReusableCell(for: indexPath) as TableViewCell - } - } - var accessoryType: UITableViewCell.AccessoryType { - switch self { - case .monthlyPayment, .minimumLength, .minimumVisits: - return .disclosureIndicator - default: - return .none - } - } - } - - // MARK: - Actions - - @objc private func allowUnverifiedValueChanged() { - ledger.allowUnverifiedPublishers = contentView.allowUnverifiedContributionsSwitch.isOn - } - - @objc private func allowVideoValueChanged() { - ledger.allowVideoContributions = contentView.allowVideoContributionsSwitch.isOn - } -} - -extension AutoContributeSettingsViewController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - guard let row = Row(rawValue: indexPath.row) else { return } - switch row { - case .monthlyPayment: - guard let wallet = ledger.walletInfo else { break } - let monthlyPayment = ledger.contributionAmount - let choices = wallet.parametersChoices.map { BATValue($0.doubleValue) } - let selectedIndex = choices.map({ $0.doubleValue }).firstIndex(of: monthlyPayment) ?? 0 - - let controller = BATValueOptionsSelectionViewController( - ledger: ledger, - options: choices, - isSelectionPrecise: false, - selectedOptionIndex: selectedIndex - ) { [weak self] selectedIndex in - guard let self = self else { return } - if selectedIndex < choices.count { - self.ledger.contributionAmount = choices[selectedIndex].doubleValue - } - self.navigationController?.popViewController(animated: true) - } - - controller.title = Strings.autoContributeMonthlyPaymentTitle - navigationController?.pushViewController(controller, animated: true) - case .minimumLength: - let choices = BraveLedger.MinimumVisitDurationOptions.allCases.map { $0.rawValue } - let selectedIndex = choices.firstIndex(of: ledger.minimumVisitDuration) ?? 0 - let controller = OptionsSelectionViewController( - options: BraveLedger.MinimumVisitDurationOptions.allCases, - selectedOptionIndex: selectedIndex) { [weak self] (selectedIndex) in - guard let self = self else { return } - if selectedIndex < choices.count { - self.ledger.minimumVisitDuration = choices[selectedIndex] - } - self.navigationController?.popViewController(animated: true) - } - controller.title = Strings.autoContributeMinimumLength - navigationController?.pushViewController(controller, animated: true) - case .minimumVisits: - let choices = BraveLedger.MinimumVisitsOptions.allCases.map { $0.rawValue } - let selectedIndex = choices.firstIndex(of: ledger.minimumNumberOfVisits) ?? 0 - let controller = OptionsSelectionViewController( - options: BraveLedger.MinimumVisitsOptions.allCases, - selectedOptionIndex: selectedIndex) { [weak self] (selectedIndex) in - guard let self = self else { return } - if selectedIndex < choices.count { - self.ledger.minimumNumberOfVisits = choices[selectedIndex] - } - self.navigationController?.popViewController(animated: true) - } - controller.title = Strings.autoContributeMinimumVisits - navigationController?.pushViewController(controller, animated: true) - default: - break - } - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return Row.allCases.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let row = Row(rawValue: indexPath.row) else { return UITableViewCell() } - // Setup - let cell = row.dequeuedCell(from: tableView, indexPath: indexPath) - cell.label.font = SettingsUX.bodyFont - cell.label.numberOfLines = 0 - cell.label.lineBreakMode = .byWordWrapping - cell.accessoryLabel?.appearanceTextColor = Colors.grey100 - cell.accessoryLabel?.font = SettingsUX.bodyFont - cell.accessoryType = row.accessoryType - switch row { - case .monthlyPayment: - cell.label.text = Strings.autoContributeMonthlyPayment - if let dollarAmount = ledger.dollarStringForBATAmount(ledger.contributionAmount) { - let amount = "\(ledger.contributionAmount) \(Strings.BAT) (\(dollarAmount))" - cell.accessoryLabel?.text = String(format: Strings.settingsAutoContributeUpToValue, amount) - } - case .minimumLength: - cell.label.text = Strings.autoContributeMinimumLengthMessage - cell.accessoryLabel?.text = BraveLedger.MinimumVisitDurationOptions(rawValue: ledger.minimumVisitDuration)?.displayString - case .minimumVisits: - cell.label.text = Strings.autoContributeMinimumVisitsMessage - cell.accessoryLabel?.text = BraveLedger.MinimumVisitsOptions(rawValue: ledger.minimumNumberOfVisits)?.displayString - case .allowUnverifiedContributions: - cell.label.text = Strings.autoContributeToUnverifiedSites - cell.accessoryView = contentView.allowUnverifiedContributionsSwitch - cell.selectionStyle = .none - case .allowVideoContributions: - cell.label.text = Strings.autoContributeToVideos - cell.accessoryView = contentView.allowVideoContributionsSwitch - cell.selectionStyle = .none - } - return cell - } -} - -extension AutoContributeSettingsViewController { - class View: UIView { - let tableView = UITableView(frame: .zero, style: .grouped) - - override init(frame: CGRect) { - super.init(frame: frame) - - tableView.backgroundView = UIView().then { $0.backgroundColor = SettingsUX.backgroundColor } - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) - tableView.separatorStyle = .none - tableView.register(TableViewCell.self) - tableView.register(Value1TableViewCell.self) - - addSubview(tableView) - tableView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - tableView.layoutMargins = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) - } - - @available(*, unavailable) - required init(coder: NSCoder) { - fatalError() - } - - let allowUnverifiedContributionsSwitch = UISwitch().then { - $0.onTintColor = BraveUX.braveOrange - } - - let allowVideoContributionsSwitch = UISwitch().then { - $0.onTintColor = BraveUX.braveOrange - } - } -} diff --git a/BraveRewardsUI/Settings/Auto-Contribute/Details/AutoContributeCell.swift b/BraveRewardsUI/Settings/Auto-Contribute/Supported List/AutoContributeCell.swift similarity index 100% rename from BraveRewardsUI/Settings/Auto-Contribute/Details/AutoContributeCell.swift rename to BraveRewardsUI/Settings/Auto-Contribute/Supported List/AutoContributeCell.swift diff --git a/BraveRewardsUI/Settings/Auto-Contribute/Supported List/AutoContributeSupportedListController.swift b/BraveRewardsUI/Settings/Auto-Contribute/Supported List/AutoContributeSupportedListController.swift new file mode 100644 index 00000000000..1d0b5ca378f --- /dev/null +++ b/BraveRewardsUI/Settings/Auto-Contribute/Supported List/AutoContributeSupportedListController.swift @@ -0,0 +1,220 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import BraveRewards + +class AutoContributeSupportedListController: UIViewController { + private let state: RewardsState + private let ledgerObserver: LedgerObserver + + init(state: RewardsState) { + self.state = state + ledgerObserver = LedgerObserver(ledger: state.ledger) + state.ledger.add(ledgerObserver) + super.init(nibName: nil, bundle: nil) + setupLedgerObservers() + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + + private var contentView: View { + return view as! View // swiftlint:disable:this force_cast + } + + override func loadView() { + view = View() + } + + private lazy var editButton = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(tappedEdit)) + private lazy var doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(tappedDone)) + + override func viewDidLoad() { + super.viewDidLoad() + contentView.tableView.delegate = self + contentView.tableView.dataSource = self + + title = Strings.autoContributeSupportedSites.capitalized + + navigationItem.rightBarButtonItem = editButton + reloadData() + } + + @objc private func tappedEdit() { + contentView.tableView.setEditing(true, animated: true) + navigationItem.setRightBarButton(doneButton, animated: true) + } + + @objc private func tappedDone() { + contentView.tableView.setEditing(false, animated: true) + navigationItem.setRightBarButton(editButton, animated: true) + } + + // MARK: - Data + + private var publishers: [PublisherInfo] = [] + + func reloadData() { + let filter = state.ledger.supportedPublishersFilter + state.ledger.listActivityInfo(fromStart: 0, limit: 0, filter: filter) { [weak self] list in + guard let self = self else { return } + self.publishers = list + self.editButton.isEnabled = !list.isEmpty + self.contentView.tableView.reloadData() + } + } + + func setupLedgerObservers() { + ledgerObserver.excludedSitesChanged = { [weak self] _, _ in + guard let self = self, self.isViewLoaded else { + return + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { + self.reloadData() + }) + } + } +} + +// MARK: - UITableViewDelegate +extension AutoContributeSupportedListController: UITableViewDelegate { + func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { + navigationItem.setRightBarButton(doneButton, animated: true) + } + + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + navigationItem.setRightBarButton(editButton, animated: true) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if !publishers.isEmpty, let url = URL(string: publishers[indexPath.row].url) { + state.delegate?.loadNewTabWithURL(url) + } + } +} + +extension AutoContributeSupportedListController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return publishers.isEmpty ? 1 : publishers.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if publishers.isEmpty { + let cell = tableView.dequeueReusableCell(for: indexPath) as EmptyTableCell + cell.label.text = Strings.emptyAutoContribution + return cell + } + guard let publisher = publishers[safe: indexPath.row] else { + assertionFailure("No Publisher found at index: \(indexPath.row)") + return UITableViewCell() + } + let cell = tableView.dequeueReusableCell(for: indexPath) as AutoContributeCell + if let url = URL(string: publisher.url) { + state.dataSource?.retrieveFavicon(for: url, faviconURL: URL(string: publisher.faviconUrl)) { data in + cell.siteImageView.image = data?.image ?? UIImage(frameworkResourceNamed: "defaultFavicon") + cell.siteImageView.backgroundColor = data?.backgroundColor + } + } + + cell.verifiedStatusImageView.isHidden = publisher.status == .notVerified + let provider = " \(publisher.provider.isEmpty ? "" : String(format: Strings.onProviderText, publisher.providerDisplayString))" + let attrName = NSMutableAttributedString(string: publisher.name).then { + $0.append(NSMutableAttributedString(string: provider, attributes: [.font: UIFont.boldSystemFont(ofSize: 14.0), + .foregroundColor: UIColor.gray])) + } + cell.siteNameLabel.attributedText = attrName + cell.attentionAmount = CGFloat(publisher.percent) + return cell + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return !publishers.isEmpty + } + + func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + if publishers.isEmpty { return .none } + return .delete + } + + func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { + return Strings.exclude + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if let publisher = publishers[safe: indexPath.row] { + tableView.performBatchUpdates({ + publishers.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .automatic) + if publishers.isEmpty { + tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) + } + }, completion: { _ in + self.state.ledger.updatePublisherExclusionState( + withId: publisher.id, + state: .excluded + ) + }) + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return contentView.headerView + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return contentView.headerView.systemLayoutSizeFitting( + CGSize(width: tableView.bounds.width, height: tableView.bounds.height), + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ).height + } +} + +extension AutoContributeSupportedListController { + private class View: UIView { + let tableView = UITableView(frame: .zero, style: .grouped) + + fileprivate let headerView = TableHeaderRowView( + columns: [ + TableHeaderRowView.Column( + title: Strings.site.uppercased(), + width: .percentage(0.7) + ), + TableHeaderRowView.Column( + title: Strings.attention.uppercased(), + width: .percentage(0.3), + align: .right + ), + ], + tintColor: BraveUX.autoContributeTintColor + ) + + override init(frame: CGRect) { + super.init(frame: frame) + + tableView.backgroundView = UIView().then { + $0.backgroundColor = SettingsUX.backgroundColor + } + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) + tableView.separatorInset = .zero + tableView.register(AutoContributeCell.self) + tableView.register(EmptyTableCell.self) + tableView.layoutMargins = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + tableView.appearanceSeparatorColor = UIColor(white: 0.85, alpha: 1.0) + + addSubview(tableView) + tableView.snp.makeConstraints { + $0.edges.equalTo(self) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + } +} diff --git a/BraveShared/AppearanceAttributes.swift b/BraveShared/AppearanceAttributes.swift index 3bbd0721a00..cf5d3a5ae43 100644 --- a/BraveShared/AppearanceAttributes.swift +++ b/BraveShared/AppearanceAttributes.swift @@ -63,6 +63,13 @@ public extension UINavigationBar { } } +public extension UIToolbar { + @objc dynamic var appearanceBarTintColor: UIColor? { + get { return self.barTintColor } + set { self.barTintColor = newValue } + } +} + public extension UIButton { @objc dynamic var appearanceTextColor: UIColor! { get { return self.titleColor(for: .normal) } diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index dde4e944e4a..2b3c10a7800 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -217,7 +217,6 @@ 271DECFB234CC7EF009DAC37 /* TipsSummaryTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271DEC88234CC7EF009DAC37 /* TipsSummaryTableCell.swift */; }; 271DECFC234CC7EF009DAC37 /* SettingsTipsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271DEC89234CC7EF009DAC37 /* SettingsTipsSectionView.swift */; }; 271DED01234CC7EF009DAC37 /* SettingsUX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271DEC8F234CC7EF009DAC37 /* SettingsUX.swift */; }; - 271DED02234CC7EF009DAC37 /* AutoContributeSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271DEC92234CC7EF009DAC37 /* AutoContributeSettingsViewController.swift */; }; 271DED03234CC7EF009DAC37 /* AutoContributeDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271DEC94234CC7EF009DAC37 /* AutoContributeDetailsViewController.swift */; }; 271DED04234CC7EF009DAC37 /* AutoContributeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271DEC95234CC7EF009DAC37 /* AutoContributeCell.swift */; }; 271DED05234CC7EF009DAC37 /* SettingsAutoContributeSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271DEC96234CC7EF009DAC37 /* SettingsAutoContributeSectionView.swift */; }; @@ -288,6 +287,9 @@ 27A586E1214C0DDD000CAE3C /* PreferencesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A586E0214C0DDD000CAE3C /* PreferencesTest.swift */; }; 27AC169323834175004BE19C /* MonthlyAdsGrantReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27AC168F23833A16004BE19C /* MonthlyAdsGrantReminder.swift */; }; 27AC169823834510004BE19C /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27AC169623834510004BE19C /* UserNotifications.framework */; }; + 27B0C7B523D24BAA00B9EA3D /* AutoContributeExclusionListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B0C7B323D24BAA00B9EA3D /* AutoContributeExclusionListController.swift */; }; + 27B0C7B823D2555C00B9EA3D /* ExclusionListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B0C7B623D2555C00B9EA3D /* ExclusionListCell.swift */; }; + 27B0C7BC23D604C500B9EA3D /* AutoContributeSupportedListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B0C7BA23D604C500B9EA3D /* AutoContributeSupportedListController.swift */; }; 27B1E26D235E58190062E86F /* LocaleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B1E26C235E58190062E86F /* LocaleExtensions.swift */; }; 27C461DE211B76500088A441 /* ShieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C461DD211B76500088A441 /* ShieldsView.swift */; }; 27C46201211CD8D20088A441 /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A176323020CF2A6000126F25 /* DeferredTestUtils.swift */; }; @@ -1459,7 +1461,6 @@ 271DEC88234CC7EF009DAC37 /* TipsSummaryTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TipsSummaryTableCell.swift; sourceTree = ""; }; 271DEC89234CC7EF009DAC37 /* SettingsTipsSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTipsSectionView.swift; sourceTree = ""; }; 271DEC8F234CC7EF009DAC37 /* SettingsUX.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsUX.swift; sourceTree = ""; }; - 271DEC92234CC7EF009DAC37 /* AutoContributeSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoContributeSettingsViewController.swift; sourceTree = ""; }; 271DEC94234CC7EF009DAC37 /* AutoContributeDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoContributeDetailsViewController.swift; sourceTree = ""; }; 271DEC95234CC7EF009DAC37 /* AutoContributeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoContributeCell.swift; sourceTree = ""; }; 271DEC96234CC7EF009DAC37 /* SettingsAutoContributeSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAutoContributeSectionView.swift; sourceTree = ""; }; @@ -1529,6 +1530,9 @@ 27A586E0214C0DDD000CAE3C /* PreferencesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTest.swift; sourceTree = ""; }; 27AC168F23833A16004BE19C /* MonthlyAdsGrantReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthlyAdsGrantReminder.swift; sourceTree = ""; }; 27AC169623834510004BE19C /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 27B0C7B323D24BAA00B9EA3D /* AutoContributeExclusionListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoContributeExclusionListController.swift; sourceTree = ""; }; + 27B0C7B623D2555C00B9EA3D /* ExclusionListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclusionListCell.swift; sourceTree = ""; }; + 27B0C7BA23D604C500B9EA3D /* AutoContributeSupportedListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoContributeSupportedListController.swift; sourceTree = ""; }; 27B1E26C235E58190062E86F /* LocaleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtensions.swift; sourceTree = ""; }; 27C461DD211B76500088A441 /* ShieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShieldsView.swift; sourceTree = ""; }; 27D114D32358FBBF00166534 /* BraveRewardsSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraveRewardsSettingsViewController.swift; sourceTree = ""; }; @@ -3155,26 +3159,18 @@ 271DEC90234CC7EF009DAC37 /* Auto-Contribute */ = { isa = PBXGroup; children = ( - 271DEC91234CC7EF009DAC37 /* Settings */, + 27B0C7B923D604B600B9EA3D /* Supported List */, + 27B0C7B123D24B9400B9EA3D /* Exclusion List */, 271DEC93234CC7EF009DAC37 /* Details */, 271DEC96234CC7EF009DAC37 /* SettingsAutoContributeSectionView.swift */, ); path = "Auto-Contribute"; sourceTree = ""; }; - 271DEC91234CC7EF009DAC37 /* Settings */ = { - isa = PBXGroup; - children = ( - 271DEC92234CC7EF009DAC37 /* AutoContributeSettingsViewController.swift */, - ); - path = Settings; - sourceTree = ""; - }; 271DEC93234CC7EF009DAC37 /* Details */ = { isa = PBXGroup; children = ( 271DEC94234CC7EF009DAC37 /* AutoContributeDetailsViewController.swift */, - 271DEC95234CC7EF009DAC37 /* AutoContributeCell.swift */, ); path = Details; sourceTree = ""; @@ -3272,6 +3268,24 @@ path = Rewards; sourceTree = ""; }; + 27B0C7B123D24B9400B9EA3D /* Exclusion List */ = { + isa = PBXGroup; + children = ( + 27B0C7B323D24BAA00B9EA3D /* AutoContributeExclusionListController.swift */, + 27B0C7B623D2555C00B9EA3D /* ExclusionListCell.swift */, + ); + path = "Exclusion List"; + sourceTree = ""; + }; + 27B0C7B923D604B600B9EA3D /* Supported List */ = { + isa = PBXGroup; + children = ( + 27B0C7BA23D604C500B9EA3D /* AutoContributeSupportedListController.swift */, + 271DEC95234CC7EF009DAC37 /* AutoContributeCell.swift */, + ); + path = "Supported List"; + sourceTree = ""; + }; 27F443962135E11200296C58 /* BraveShareTo */ = { isa = PBXGroup; children = ( @@ -5620,6 +5634,7 @@ 271DED13234CC7EF009DAC37 /* LinkLabel.swift in Sources */, 271DED21234CC7EF009DAC37 /* BasicAnimationController.swift in Sources */, 271DED26234CC7EF009DAC37 /* BraveLedgerExtensions.swift in Sources */, + 27B0C7BC23D604C500B9EA3D /* AutoContributeSupportedListController.swift in Sources */, 271DECD8234CC7EF009DAC37 /* TippingConfirmationView.swift in Sources */, 271DED22234CC7EF009DAC37 /* DismissButton.swift in Sources */, 271DED29234CC7EF009DAC37 /* PublisherInfoExtensions.swift in Sources */, @@ -5649,7 +5664,6 @@ 271DECC4234CC7EF009DAC37 /* PublisherAttentionView.swift in Sources */, 27253973236CE6B100D06EF1 /* GrantClaimedViewController.swift in Sources */, 271DED23234CC7EF009DAC37 /* UITableViewExtensions.swift in Sources */, - 271DED02234CC7EF009DAC37 /* AutoContributeSettingsViewController.swift in Sources */, 271DECD1234CC7EF009DAC37 /* AdView.swift in Sources */, 271DECE9234CC7EF009DAC37 /* BATValue.swift in Sources */, 271DECCB234CC7EF009DAC37 /* CreateWalletViewController.swift in Sources */, @@ -5710,6 +5724,7 @@ 271DED1D234CC7EF009DAC37 /* Colors.swift in Sources */, 271DED24234CC7EF009DAC37 /* ArrayExtensions.swift in Sources */, 271DECD7234CC7EF009DAC37 /* TippingViewController.swift in Sources */, + 27B0C7B823D2555C00B9EA3D /* ExclusionListCell.swift in Sources */, 271DECF5234CC7EF009DAC37 /* SettingsGrantSectionView.swift in Sources */, 271DECE2234CC7EF009DAC37 /* WalletViewController.swift in Sources */, 271DED1F234CC7EF009DAC37 /* FaviconData.swift in Sources */, @@ -5718,6 +5733,7 @@ 271DECE0234CC7EF009DAC37 /* WalletHeaderView.swift in Sources */, 271DECEE234CC7EF009DAC37 /* RewardsSummaryView.swift in Sources */, 271DECC7234CC7EF009DAC37 /* RewardsDisabledView.swift in Sources */, + 27B0C7B523D24BAA00B9EA3D /* AutoContributeExclusionListController.swift in Sources */, 271DECE8234CC7EF009DAC37 /* RewardsNotificationViewBuilder.swift in Sources */, 271DECEB234CC7EF009DAC37 /* GrantsListViewController.swift in Sources */, 271DECE4234CC7EF009DAC37 /* WalletView.swift in Sources */,