diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0743f5b72..07c93f2d3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -160,6 +160,7 @@ D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; }; D83B54F82C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */; }; D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */; }; + D84BB76B2C3BE33800493718 /* NotificationPolicyFilterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84BB76A2C3BE33800493718 /* NotificationPolicyFilterTableViewCell.swift */; }; D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; }; D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; }; D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */; }; @@ -794,6 +795,7 @@ D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = ""; tabWidth = 4; }; D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFilteringBannerTableViewCell.swift; sourceTree = ""; }; D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusPill.swift; sourceTree = ""; }; + D84BB76A2C3BE33800493718 /* NotificationPolicyFilterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPolicyFilterTableViewCell.swift; sourceTree = ""; }; D84C099D2B0F9E33009E685E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; D84C099F2B0F9E41009E685E /* Setup.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Setup.md; sourceTree = ""; }; D84C09A02B0F9E41009E685E /* How-it-works.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "How-it-works.md"; sourceTree = ""; }; @@ -1767,6 +1769,7 @@ isa = PBXGroup; children = ( D80EC2622C2978EE009724A5 /* NotificationPolicyViewController.swift */, + D84BB76A2C3BE33800493718 /* NotificationPolicyFilterTableViewCell.swift */, ); path = "Notification Filtering"; sourceTree = ""; @@ -3559,6 +3562,7 @@ DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */, 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */, DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */, + D84BB76B2C3BE33800493718 /* NotificationPolicyFilterTableViewCell.swift in Sources */, DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */, DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */, D8FAAE412AD0475900DC1832 /* AboutInstanceTableViewHeader.swift in Sources */, diff --git a/Mastodon/Scene/Notification/Notification Filtering/NotificationPolicyFilterTableViewCell.swift b/Mastodon/Scene/Notification/Notification Filtering/NotificationPolicyFilterTableViewCell.swift new file mode 100644 index 000000000..e2e59d259 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/NotificationPolicyFilterTableViewCell.swift @@ -0,0 +1,49 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import UIKit + +protocol NotificationPolicyFilterTableViewCellDelegate: AnyObject { + func toggleValueChanged(_ tableViewCell: NotificationPolicyFilterTableViewCell, filterItem: NotificationFilterItem, newValue: Bool) +} + +class NotificationPolicyFilterTableViewCell: ToggleTableViewCell { + override class var reuseIdentifier: String { + return "NotificationPolicyFilterTableViewCell" + } + + var filterItem: NotificationFilterItem? + weak var delegate: NotificationPolicyFilterTableViewCellDelegate? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + toggle.addTarget(self, action: #selector(NotificationPolicyFilterTableViewCell.toggleValueChanged(_:)), for: .valueChanged) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + public func configure(with filterItem: NotificationFilterItem, viewModel: NotificationFilterViewModel) { + label.text = filterItem.title + self.filterItem = filterItem + + let toggleIsOn: Bool + switch filterItem { + case .notFollowing: + toggleIsOn = viewModel.notFollowing + case .noFollower: + toggleIsOn = viewModel.noFollower + case .newAccount: + toggleIsOn = viewModel.newAccount + case .privateMentions: + toggleIsOn = viewModel.privateMentions + } + + toggle.isOn = toggleIsOn + } + + @objc func toggleValueChanged(_ sender: UISwitch) { + guard let filterItem, let delegate else { return } + + delegate.toggleValueChanged(self, filterItem: filterItem, newValue: sender.isOn) + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/NotificationPolicyViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/NotificationPolicyViewController.swift index 0aff29b49..9dd3dfe6e 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/NotificationPolicyViewController.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/NotificationPolicyViewController.swift @@ -1,13 +1,134 @@ // Copyright © 2024 Mastodon gGmbH. All rights reserved. import UIKit +import MastodonLocalization + +enum NotificationFilterSection: Hashable { + case main +} + +enum NotificationFilterItem: Hashable, CaseIterable { + case notFollowing + case noFollower + case newAccount + case privateMentions + + var title: String { + switch self { + case .notFollowing: + return "People you don't follow" + case .noFollower: + return "People not following you" + case .newAccount: + return "New accounts" + case .privateMentions: + return "Unsolicited private mentions" + } + } +} + +struct NotificationFilterViewModel { + var notFollowing: Bool + var noFollower: Bool + var newAccount: Bool + var privateMentions: Bool + + init(notFollowing: Bool, noFollower: Bool, newAccount: Bool, privateMentions: Bool) { + self.notFollowing = notFollowing + self.noFollower = noFollower + self.newAccount = newAccount + self.privateMentions = privateMentions + } +} class NotificationPolicyViewController: UIViewController { + + //TODO: DataSource, Source, Items + let tableView: UITableView + var dataSource: UITableViewDiffableDataSource? + let items: [NotificationFilterItem] + let viewModel: NotificationFilterViewModel + + init() { + //TODO: Dependency Inject Policy ViewModel + viewModel = NotificationFilterViewModel(notFollowing: false, noFollower: false, newAccount: false, privateMentions: false) + items = NotificationFilterItem.allCases + + tableView = UITableView(frame: .zero, style: .insetGrouped) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.register(NotificationPolicyFilterTableViewCell.self, forCellReuseIdentifier: NotificationPolicyFilterTableViewCell.reuseIdentifier) + super.init(nibName: nil, bundle: nil) - view.backgroundColor = .systemGreen + let dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in + guard let self, let cell = tableView.dequeueReusableCell(withIdentifier: NotificationPolicyFilterTableViewCell.reuseIdentifier, for: indexPath) as? NotificationPolicyFilterTableViewCell else { + fatalError("No NotificationPolicyFilterTableViewCell") + } + + //TODO: Configuration + + let item = items[indexPath.row] + cell.configure(with: item, viewModel: self.viewModel) + cell.delegate = self + + return cell + } + + tableView.dataSource = dataSource + tableView.delegate = self + + self.dataSource = dataSource + view.addSubview(tableView) + view.backgroundColor = .systemGroupedBackground + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.save, style: .done, target: self, action: #selector(NotificationPolicyViewController.save(_:))) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .done, target: self, action: #selector(NotificationPolicyViewController.cancel(_:))) + + setupConstraints() } - + + override func viewDidLoad() { + super.viewDidLoad() + + var snapshot = NSDiffableDataSourceSnapshot() + + snapshot.appendSections([.main]) + snapshot.appendItems(items) + + dataSource?.apply(snapshot, animatingDifferences: false) + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + let constraints = [ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor), + view.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + ] + + NSLayoutConstraint.activate(constraints) + } + + // MARK: - Action + + @objc private func save(_ sender: UIBarButtonItem) { + //TODO: Save + } + + @objc private func cancel(_ sender: UIBarButtonItem) { + dismiss(animated: true) + } +} + +extension NotificationPolicyViewController: UITableViewDelegate { + +} + +extension NotificationPolicyViewController: NotificationPolicyFilterTableViewCellDelegate { + func toggleValueChanged(_ tableViewCell: NotificationPolicyFilterTableViewCell, filterItem: NotificationFilterItem, newValue: Bool) { + //TODO: Update ViewModel + } }