diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 3be531abf..f325e7d72 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -765,7 +765,6 @@ "multiple_people_boosted": "%@ boosted:", "multiple_people_favourited": "%@ favorited:", "multiple_people_followed_you": "%@ followed you", - "mulitple_people_signed_up": "%@ new signups", "single_name_signed_up": "%@ signed up", "someone_reported_account_for_rule_violation": "Someone reported %@ for rule violation.", "someone_reported_posts_from_account_for_rule_violation": "Someone reported %@ from %@ for rule violation." @@ -827,16 +826,34 @@ "private_mentions": { "title": "Unsolicited private mentions", "subtitle": "Filtered unless it’s in reply to your own mention or if you follow the sender" + }, + "moderated_accounts": { + "title": "Moderated accounts", + "subtitle": "Limited by server moderators" + }, + "action": { + "filter": { + "title": "Filter", + "subtitle": "Send to filtered notifications inbox" + }, + "accept": { + "title": "Accept", + "subtitle": "Show in notifications" + }, + "drop": { + "title": "Ignore", + "subtitle": "Send to the void, never to be seen again" + } } }, "admin_filter": { "reports": { "title": "Admin reports", - "subtitle": "Hide reports of spam, rule violations, and other complaints on this instance" + "subtitle": "Show reports of spam, rule violations, and other complaints on this instance" }, "signups": { "title": "Account signups", - "subtitle": "Hide notifications of new accounts created on this instance" + "subtitle": "Show notifications of new accounts created on this instance" } } }, diff --git a/Localization/app.json b/Localization/app.json index c5d2ece30..f325e7d72 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -826,16 +826,34 @@ "private_mentions": { "title": "Unsolicited private mentions", "subtitle": "Filtered unless it’s in reply to your own mention or if you follow the sender" + }, + "moderated_accounts": { + "title": "Moderated accounts", + "subtitle": "Limited by server moderators" + }, + "action": { + "filter": { + "title": "Filter", + "subtitle": "Send to filtered notifications inbox" + }, + "accept": { + "title": "Accept", + "subtitle": "Show in notifications" + }, + "drop": { + "title": "Ignore", + "subtitle": "Send to the void, never to be seen again" + } } }, "admin_filter": { "reports": { "title": "Admin reports", - "subtitle": "Hide reports of spam, rule violations, and other complaints on this instance" + "subtitle": "Show reports of spam, rule violations, and other complaints on this instance" }, "signups": { "title": "Account signups", - "subtitle": "Hide notifications of new accounts created on this instance" + "subtitle": "Show notifications of new accounts created on this instance" } } }, diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift index d7890f8d2..a9cc32530 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift @@ -70,16 +70,17 @@ class NotificationListViewController: UIHostingController if let existingPreferences = await BodegaPersistence.Notifications.currentPreferences(for: user.authentication) { return existingPreferences } else { - return AdminNotificationFilterSettings(filterOutReports: false, filterOutSignups: false) + return AdminNotificationFilterSettings(forReports: .accept, forSignups: .accept) } }() let policyViewModel = await NotificationFilterViewModel( NotificationFilterSettings( - notFollowing: policy.filterNotFollowing, - noFollower: policy.filterNotFollowers, - newAccount: policy.filterNewAccounts, - privateMentions: policy.filterPrivateMentions + forNotFollowing: policy.forNotFollowing, + forNotFollowers: policy.forNotFollowers, + forNewAccounts: policy.forNewAccounts, + forPrivateMentions: policy.forPrivateMentions, + forLimitedAccounts: policy.forLimitedAccounts ), adminSettings: adminSettings ) diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift index 4b5b38ebe..83bba7a85 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift @@ -1,37 +1,127 @@ // Copyright © 2024 Mastodon gGmbH. All rights reserved. +import MastodonSDK import UIKit +import MastodonLocalization +import MastodonAsset protocol NotificationPolicyFilterTableViewCellDelegate: AnyObject { - func toggleValueChanged(_ tableViewCell: NotificationPolicyFilterTableViewCell, filterItem: NotificationFilterItem, newValue: Bool) + func pickerValueChanged( + _ tableViewCell: NotificationPolicyFilterTableViewCell, + filterItem: NotificationFilterItem, + newValue: Mastodon.Entity.NotificationPolicy.NotificationFilterAction) } -class NotificationPolicyFilterTableViewCell: ToggleTableViewCell { +class NotificationPolicyFilterTableViewCell: TrailingButtonTableViewCell +{ + typealias FilterActionOption = Mastodon.Entity.NotificationPolicy.NotificationFilterAction + override class var reuseIdentifier: String { return "NotificationPolicyFilterTableViewCell" } + private let options: + [FilterActionOption] = [ + .accept, .filter, .drop, + ] + var filterItem: NotificationFilterItem? weak var delegate: NotificationPolicyFilterTableViewCellDelegate? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) + label.font = UIFontMetrics(forTextStyle: .body).scaledFont( + for: .systemFont(ofSize: 17, weight: .regular)) subtitleLabel.textColor = .secondaryLabel - subtitleLabel.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + subtitleLabel.font = UIFontMetrics(forTextStyle: .subheadline) + .scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + } - toggle.addTarget(self, action: #selector(NotificationPolicyFilterTableViewCell.toggleValueChanged(_:)), for: .valueChanged) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func indexForOption(_ option: FilterActionOption) -> Int { + return options.firstIndex(of: option) ?? 0 + } - public func configure(with filterItem: NotificationFilterItem, viewModel: NotificationFilterViewModel) { + public func configure( + with filterItem: NotificationFilterItem, + viewModel: NotificationFilterViewModel + ) { label.text = filterItem.title subtitleLabel.text = filterItem.subtitle self.filterItem = filterItem - let toggleIsOn = viewModel.value(forItem: filterItem) + let buttonTitle: String + switch viewModel.value(forItem: filterItem) { + case .accept: buttonTitle = L10n.Scene.Notification.Policy.Action.Accept.title + case .filter: buttonTitle = L10n.Scene.Notification.Policy.Action.Filter.title + case .drop: buttonTitle = L10n.Scene.Notification.Policy.Action.Drop.title + case ._other(let string): buttonTitle = string + } + button.configuration = .bordered() + button.configuration?.title = buttonTitle + button.configuration?.background.strokeColor = Asset.Colors.Brand.blurple.color + button.configuration?.baseForegroundColor = Asset.Colors.Brand.blurple.color + button.showsMenuAsPrimaryAction = true + + let menuActions = [FilterActionOption.accept, .filter, .drop].map { option in + UIAction(title: option.displayTitle, subtitle: option.displaySubtitle, state: viewModel.value(forItem: filterItem) == option ? .on : .off, handler: { [weak self] _ in + guard let self else { return } + self.delegate?.pickerValueChanged(self, filterItem: filterItem, newValue: option) + }) + } + + button.menu = UIMenu.init(children: menuActions) + } +} + +protocol NotificationAdminFilterTableViewCellDelegate: AnyObject { + func toggleValueChanged( + _ tableViewCell: NotificationAdminFilterTableViewCell, + filterItem: NotificationFilterItem, + newValue: Bool) +} + +class NotificationAdminFilterTableViewCell: ToggleTableViewCell { + override class var reuseIdentifier: String { + return "NotificationAdminFilterTableViewCell" + } + + var filterItem: NotificationFilterItem? + weak var delegate: NotificationAdminFilterTableViewCellDelegate? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + label.font = UIFontMetrics(forTextStyle: .body).scaledFont( + for: .systemFont(ofSize: 17, weight: .regular)) + subtitleLabel.textColor = .secondaryLabel + subtitleLabel.font = UIFontMetrics(forTextStyle: .subheadline) + .scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + + toggle.addTarget( + self, + action: #selector( + NotificationAdminFilterTableViewCell.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 + subtitleLabel.text = filterItem.subtitle + self.filterItem = filterItem + + let toggleIsOn = viewModel.value(forItem: filterItem) == .accept toggle.isOn = toggleIsOn } @@ -39,6 +129,27 @@ class NotificationPolicyFilterTableViewCell: ToggleTableViewCell { @objc func toggleValueChanged(_ sender: UISwitch) { guard let filterItem, let delegate else { return } - delegate.toggleValueChanged(self, filterItem: filterItem, newValue: sender.isOn) + delegate.toggleValueChanged( + self, filterItem: filterItem, newValue: sender.isOn) + } +} + +extension Mastodon.Entity.NotificationPolicy.NotificationFilterAction { + var displayTitle: String { + switch self { + case .accept: return L10n.Scene.Notification.Policy.Action.Accept.title + case .filter: return L10n.Scene.Notification.Policy.Action.Filter.title + case .drop: return L10n.Scene.Notification.Policy.Action.Drop.title + case ._other(let string): return string + } + } + + var displaySubtitle: String { + switch self { + case .accept: return L10n.Scene.Notification.Policy.Action.Accept.subtitle + case .filter: return L10n.Scene.Notification.Policy.Action.Filter.subtitle + case .drop: return L10n.Scene.Notification.Policy.Action.Drop.subtitle + case ._other: return "" + } } } diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift index 953de77e7..3bdd0bbf4 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift @@ -13,22 +13,30 @@ enum NotificationFilterSection: Hashable { enum NotificationFilterItem: Hashable { case notFollowing - case noFollower - case newAccount + case notFollowers + case newAccounts case privateMentions + case limitedAccounts + case adminReports case adminSignups + + static let regularOptions = [Self.notFollowing, .notFollowers, .newAccounts, .privateMentions, .limitedAccounts] + static let adminOptions = [Self.adminReports, .adminSignups] var title: String { switch self { case .notFollowing: return L10n.Scene.Notification.Policy.NotFollowing.title - case .noFollower: + case .notFollowers: return L10n.Scene.Notification.Policy.NoFollower.title - case .newAccount: + case .newAccounts: return L10n.Scene.Notification.Policy.NewAccount.title case .privateMentions: return L10n.Scene.Notification.Policy.PrivateMentions.title + case .limitedAccounts: + return L10n.Scene.Notification.Policy.ModeratedAccounts.title + case .adminReports: return L10n.Scene.Notification.AdminFilter.Reports.title case .adminSignups: @@ -40,12 +48,15 @@ enum NotificationFilterItem: Hashable { switch self { case .notFollowing: return L10n.Scene.Notification.Policy.NotFollowing.subtitle - case .noFollower: + case .notFollowers: return L10n.Scene.Notification.Policy.NoFollower.subtitle - case .newAccount: + case .newAccounts: return L10n.Scene.Notification.Policy.NewAccount.subtitle case .privateMentions: return L10n.Scene.Notification.Policy.PrivateMentions.subtitle + case .limitedAccounts: + return L10n.Scene.Notification.Policy.ModeratedAccounts.subtitle + case .adminReports: return L10n.Scene.Notification.AdminFilter.Reports.subtitle case .adminSignups: @@ -55,21 +66,22 @@ enum NotificationFilterItem: Hashable { } struct NotificationFilterSettings: Codable, Equatable { - let notFollowing: Bool - let noFollower: Bool - let newAccount: Bool - let privateMentions: Bool + let forNotFollowing: Mastodon.Entity.NotificationPolicy.NotificationFilterAction + let forNotFollowers: Mastodon.Entity.NotificationPolicy.NotificationFilterAction + let forNewAccounts: Mastodon.Entity.NotificationPolicy.NotificationFilterAction + let forPrivateMentions: Mastodon.Entity.NotificationPolicy.NotificationFilterAction + let forLimitedAccounts: Mastodon.Entity.NotificationPolicy.NotificationFilterAction } struct AdminNotificationFilterSettings: Codable, Equatable { - let filterOutReports: Bool - let filterOutSignups: Bool + let forReports: Mastodon.Entity.NotificationPolicy.NotificationFilterAction + let forSignups: Mastodon.Entity.NotificationPolicy.NotificationFilterAction var excludedNotificationTypes: [Mastodon.Entity.NotificationType]? { var excluded = [Mastodon.Entity.NotificationType]() - if filterOutReports { + if forReports != .accept { excluded.append(.adminReport) } - if filterOutSignups { + if forSignups != .accept { excluded.append(.adminSignUp) } return excluded.isEmpty ? nil : excluded @@ -93,59 +105,73 @@ class NotificationFilterViewModel { self.adminFilterSettings = adminSettings } - func value(forItem item: NotificationFilterItem) -> Bool { + func value(forItem item: NotificationFilterItem) -> Mastodon.Entity.NotificationPolicy.NotificationFilterAction { switch item { case .notFollowing: - return regularFilterSettings.notFollowing - case .noFollower: - return regularFilterSettings.noFollower - case .newAccount: - return regularFilterSettings.newAccount + return regularFilterSettings.forNotFollowing + case .notFollowers: + return regularFilterSettings.forNotFollowers + case .newAccounts: + return regularFilterSettings.forNewAccounts case .privateMentions: - return regularFilterSettings.privateMentions + return regularFilterSettings.forPrivateMentions + case .limitedAccounts: + return regularFilterSettings.forLimitedAccounts case .adminReports: - return adminFilterSettings?.filterOutReports ?? true + return adminFilterSettings?.forReports ?? .drop case .adminSignups: - return adminFilterSettings?.filterOutSignups ?? true + return adminFilterSettings?.forSignups ?? .drop } } - func setValue(_ value: Bool, forItem item: NotificationFilterItem) { + func setValue(_ value: Mastodon.Entity.NotificationPolicy.NotificationFilterAction, forItem item: NotificationFilterItem) { switch item { case .notFollowing: regularFilterSettings = NotificationFilterSettings( - notFollowing: value, - noFollower: regularFilterSettings.noFollower, - newAccount: regularFilterSettings.newAccount, - privateMentions: regularFilterSettings.privateMentions) - case .noFollower: + forNotFollowing: value, + forNotFollowers: regularFilterSettings.forNotFollowers, + forNewAccounts: regularFilterSettings.forNewAccounts, + forPrivateMentions: regularFilterSettings.forPrivateMentions, + forLimitedAccounts: regularFilterSettings.forLimitedAccounts) + case .notFollowers: regularFilterSettings = NotificationFilterSettings( - notFollowing: regularFilterSettings.notFollowing, - noFollower: value, - newAccount: regularFilterSettings.newAccount, - privateMentions: regularFilterSettings.privateMentions) - case .newAccount: + forNotFollowing: regularFilterSettings.forNotFollowing, + forNotFollowers: value, + forNewAccounts: regularFilterSettings.forNewAccounts, + forPrivateMentions: regularFilterSettings.forPrivateMentions, + forLimitedAccounts: regularFilterSettings.forLimitedAccounts) + case .newAccounts: regularFilterSettings = NotificationFilterSettings( - notFollowing: regularFilterSettings.notFollowing, - noFollower: regularFilterSettings.noFollower, - newAccount: value, - privateMentions: regularFilterSettings.privateMentions) + forNotFollowing: regularFilterSettings.forNotFollowing, + forNotFollowers: regularFilterSettings.forNotFollowers, + forNewAccounts: value, + forPrivateMentions: regularFilterSettings.forPrivateMentions, + forLimitedAccounts: regularFilterSettings.forLimitedAccounts) case .privateMentions: regularFilterSettings = NotificationFilterSettings( - notFollowing: regularFilterSettings.notFollowing, - noFollower: regularFilterSettings.noFollower, - newAccount: regularFilterSettings.newAccount, - privateMentions: value) + forNotFollowing: regularFilterSettings.forNotFollowing, + forNotFollowers: regularFilterSettings.forNotFollowers, + forNewAccounts: regularFilterSettings.forNewAccounts, + forPrivateMentions: value, + forLimitedAccounts: regularFilterSettings.forLimitedAccounts) + case .limitedAccounts: + regularFilterSettings = NotificationFilterSettings( + forNotFollowing: regularFilterSettings.forNotFollowing, + forNotFollowers: regularFilterSettings.forNotFollowers, + forNewAccounts: regularFilterSettings.forNewAccounts, + forPrivateMentions: regularFilterSettings.forPrivateMentions, + forLimitedAccounts: value) + case .adminReports: guard let adminFilterSettings else { return } self.adminFilterSettings = AdminNotificationFilterSettings( - filterOutReports: value, - filterOutSignups: adminFilterSettings.filterOutSignups) + forReports: value, + forSignups: adminFilterSettings.forSignups) case .adminSignups: guard let adminFilterSettings else { return } self.adminFilterSettings = AdminNotificationFilterSettings( - filterOutReports: adminFilterSettings.filterOutReports, - filterOutSignups: value) + forReports: adminFilterSettings.forReports, + forSignups: value) } } } @@ -172,7 +198,7 @@ class NotificationPolicyViewController: UIViewController { init(viewModel: NotificationFilterViewModel) { self.viewModel = viewModel - regularItems = [.notFollowing, .noFollower, .newAccount, .privateMentions] + regularItems = [.notFollowing, .notFollowers, .newAccounts, .privateMentions, .limitedAccounts] adminItems = [.adminReports, .adminSignups] headerBar = NotificationPolicyHeaderView() @@ -182,8 +208,12 @@ class NotificationPolicyViewController: UIViewController { tableView.translatesAutoresizingMaskIntoConstraints = false tableView.register( NotificationPolicyFilterTableViewCell.self, - forCellReuseIdentifier: NotificationPolicyFilterTableViewCell - .reuseIdentifier) + forCellReuseIdentifier: NotificationPolicyFilterTableViewCell.reuseIdentifier + ) + tableView.register( + NotificationAdminFilterTableViewCell.self, + forCellReuseIdentifier: NotificationAdminFilterTableViewCell.reuseIdentifier + ) tableView.contentInset.top = -20 super.init(nibName: nil, bundle: nil) @@ -192,15 +222,14 @@ class NotificationPolicyViewController: UIViewController { NotificationFilterSection, NotificationFilterItem >(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in - guard let self, - let cell = tableView.dequeueReusableCell( - withIdentifier: NotificationPolicyFilterTableViewCell - .reuseIdentifier, for: indexPath) - as? NotificationPolicyFilterTableViewCell - else { + guard let self else { fatalError("No NotificationPolicyFilterTableViewCell") } - + + let cell = tableView.dequeueReusableCell( + withIdentifier: indexPath.section == 0 ? NotificationPolicyFilterTableViewCell + .reuseIdentifier: NotificationAdminFilterTableViewCell.reuseIdentifier, for: indexPath) + let item: NotificationFilterItem? switch indexPath.section { case 0: @@ -212,9 +241,13 @@ class NotificationPolicyViewController: UIViewController { assertionFailure() } guard let item else { return nil } - cell.configure(with: item, viewModel: self.viewModel) - cell.delegate = self - + if let cell = cell as? NotificationAdminFilterTableViewCell { + cell.configure(with: item, viewModel: self.viewModel) + cell.delegate = self + } else if let cell = cell as? NotificationPolicyFilterTableViewCell { + cell.configure(with: item, viewModel: self.viewModel) + cell.delegate = self + } return cell } @@ -241,7 +274,7 @@ class NotificationPolicyViewController: UIViewController { snapshot.appendSections([.main]) snapshot.appendItems(regularItems) - if let adminFilterSettings = viewModel.adminFilterSettings { + if viewModel.adminFilterSettings != nil { snapshot.appendSections([.admin]) snapshot.appendItems(adminItems) } @@ -289,10 +322,11 @@ class NotificationPolicyViewController: UIViewController { let updatedPolicy = try await APIService.shared .updateNotificationPolicy( authenticationBox: authenticationBox, - filterNotFollowing: viewModel.value(forItem: .notFollowing), - filterNotFollowers: viewModel.value(forItem: .noFollower), - filterNewAccounts: viewModel.value(forItem: .newAccount), - filterPrivateMentions: viewModel.value(forItem: .privateMentions) + forNotFollowing: viewModel.value(forItem: .notFollowing), + forNotFollowers: viewModel.value(forItem: .notFollowers), + forNewAccounts: viewModel.value(forItem: .newAccounts), + forPrivateMentions: viewModel.value(forItem: .privateMentions), + forLimitedAccounts: viewModel.value(forItem: .limitedAccounts) ).value delegate?.policyUpdated(self, newPolicy: updatedPolicy) @@ -319,19 +353,11 @@ extension NotificationPolicyViewController: UITableViewDelegate { ) { tableView.deselectRow(at: indexPath, animated: true) - let filterItem: NotificationFilterItem? = { - switch indexPath.section { - case 0: - return regularItems[indexPath.row] - case 1: - return adminItems[indexPath.row] - default: - return nil - } - }() + guard indexPath.section == 1 else { return } + let filterItem: NotificationFilterItem? = adminItems[indexPath.row] guard let filterItem else { return } - let currentValue = viewModel.value(forItem: filterItem) - viewModel.setValue(!currentValue, forItem: filterItem) + let currentValue = viewModel.value(forItem: filterItem) == .accept + setBool(!currentValue, forItem: filterItem) if let snapshot = dataSource?.snapshot() { dataSource?.applySnapshotUsingReloadData(snapshot) @@ -339,13 +365,28 @@ extension NotificationPolicyViewController: UITableViewDelegate { } } +extension NotificationPolicyViewController { + func setBool(_ boolValue: Bool, forItem filterItem: NotificationFilterItem) { + let option = boolValue ? Mastodon.Entity.NotificationPolicy.NotificationFilterAction.accept : .drop + viewModel.setValue(option, forItem: filterItem) + tableView.reloadData() + } +} + extension NotificationPolicyViewController: NotificationPolicyFilterTableViewCellDelegate { - func toggleValueChanged( - _ tableViewCell: NotificationPolicyFilterTableViewCell, - filterItem: NotificationFilterItem, newValue: Bool - ) { + func pickerValueChanged(_ tableViewCell: NotificationPolicyFilterTableViewCell, filterItem: NotificationFilterItem, newValue: MastodonSDK.Mastodon.Entity.NotificationPolicy.NotificationFilterAction) { viewModel.setValue(newValue, forItem: filterItem) + tableView.reloadData() + } +} + +extension NotificationPolicyViewController : NotificationAdminFilterTableViewCellDelegate { + func toggleValueChanged( + _ tableViewCell: NotificationAdminFilterTableViewCell, + filterItem: NotificationFilterItem, newValue: Bool + ) { + setBool(newValue, forItem: filterItem) } } diff --git a/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift b/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift index db47a7f75..ce3caeed6 100644 --- a/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift +++ b/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift @@ -56,3 +56,58 @@ class ToggleTableViewCell: UITableViewCell { NSLayoutConstraint.activate(constraints) } } + +class TrailingButtonTableViewCell: UITableViewCell { + class var reuseIdentifier: String { + return "TrailingButtonTableViewCell" + } + + let label: UILabel + let subtitleLabel: UILabel + private let labelStackView: UIStackView + let button: UIButton + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + + label = UILabel() + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) + label.numberOfLines = 0 + + subtitleLabel = UILabel() + subtitleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + subtitleLabel.numberOfLines = 0 + + labelStackView = UIStackView(arrangedSubviews: [label, subtitleLabel]) + labelStackView.translatesAutoresizingMaskIntoConstraints = false + labelStackView.alignment = .leading + labelStackView.axis = .vertical + labelStackView.spacing = 4 + + let buttonConfiguration = UIButton.Configuration.bordered() + button = UIButton.init(configuration: buttonConfiguration) + button.translatesAutoresizingMaskIntoConstraints = false + + super.init(style: style, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(labelStackView) + contentView.addSubview(button) + setupConstraints() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + button.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + let constraints = [ + labelStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), + labelStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + contentView.bottomAnchor.constraint(equalTo: labelStackView.bottomAnchor, constant: 11), + + button.leadingAnchor.constraint(greaterThanOrEqualTo: labelStackView.trailingAnchor, constant: 16), + button.topAnchor.constraint(equalTo: labelStackView.topAnchor), + contentView.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: 16), + ] + NSLayoutConstraint.activate(constraints) + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index f73c0a239..71392a3ac 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -136,14 +136,15 @@ extension APIService { public func updateNotificationPolicy( authenticationBox: MastodonAuthenticationBox, - filterNotFollowing: Bool, - filterNotFollowers: Bool, - filterNewAccounts: Bool, - filterPrivateMentions: Bool + forNotFollowing: Mastodon.Entity.NotificationPolicy.NotificationFilterAction, + forNotFollowers: Mastodon.Entity.NotificationPolicy.NotificationFilterAction, + forNewAccounts: Mastodon.Entity.NotificationPolicy.NotificationFilterAction, + forPrivateMentions: Mastodon.Entity.NotificationPolicy.NotificationFilterAction, + forLimitedAccounts: Mastodon.Entity.NotificationPolicy.NotificationFilterAction ) async throws -> Mastodon.Response.Content { let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization - let query = Mastodon.API.Notifications.UpdateNotificationPolicyQuery(filterNotFollowing: filterNotFollowing, filterNotFollowers: filterNotFollowers, filterNewAccounts: filterNewAccounts, filterPrivateMentions: filterPrivateMentions) + let query = Mastodon.API.Notifications.UpdateNotificationPolicyQuery(forNotFollowing: forNotFollowing, forNotFollowers: forNotFollowers, forNewAccounts: forNewAccounts, forPrivateMentions: forPrivateMentions, forLimitedAccounts: forLimitedAccounts) let response = try await Mastodon.API.Notifications.updateNotificationPolicy( session: session, diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index a64c363e2..afada161a 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -945,14 +945,14 @@ public enum L10n { public static let viewReport = L10n.tr("Localizable", "Scene.Notification.ViewReport", fallback: "View report") public enum AdminFilter { public enum Reports { - /// Hide reports of spam, rule violations, and other complaints - public static let subtitle = L10n.tr("Localizable", "Scene.Notification.AdminFilter.Reports.Subtitle", fallback: "Hide reports of spam, rule violations, and other complaints") + /// Show reports of spam, rule violations, and other complaints + public static let subtitle = L10n.tr("Localizable", "Scene.Notification.AdminFilter.Reports.Subtitle", fallback: "Show reports of spam, rule violations, and other complaints") /// Admin reports public static let title = L10n.tr("Localizable", "Scene.Notification.AdminFilter.Reports.Title", fallback: "Admin reports") } public enum Signups { - /// Hide notifications of new accounts created on this instance - public static let subtitle = L10n.tr("Localizable", "Scene.Notification.AdminFilter.Signups.Subtitle", fallback: "Hide notifications of new accounts created on this instance") + /// Show notifications of new accounts created on this instance + public static let subtitle = L10n.tr("Localizable", "Scene.Notification.AdminFilter.Signups.Subtitle", fallback: "Show notifications of new accounts created on this instance") /// Account signups public static let title = L10n.tr("Localizable", "Scene.Notification.AdminFilter.Signups.Title", fallback: "Account signups") } @@ -1102,6 +1102,32 @@ public enum L10n { public enum Policy { /// Filter Notifications from… public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.Title", fallback: "Filter Notifications from…") + public enum Action { + public enum Accept { + /// Show in notifications + public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.Action.Accept.Subtitle", fallback: "Show in notifications") + /// Accept + public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.Action.Accept.Title", fallback: "Accept") + } + public enum Drop { + /// Send to the void, never to be seen again + public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.Action.Drop.Subtitle", fallback: "Send to the void, never to be seen again") + /// Ignore + public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.Action.Drop.Title", fallback: "Ignore") + } + public enum Filter { + /// Send to filtered notifications inbox + public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.Action.Filter.Subtitle", fallback: "Send to filtered notifications inbox") + /// Filter + public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.Action.Filter.Title", fallback: "Filter") + } + } + public enum ModeratedAccounts { + /// Limited by server moderators + public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.ModeratedAccounts.Subtitle", fallback: "Limited by server moderators") + /// Moderated accounts + public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.ModeratedAccounts.Title", fallback: "Moderated accounts") + } public enum NewAccount { /// Created within the past 30 days public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.NewAccount.Subtitle", fallback: "Created within the past 30 days") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 00f022de8..6e4422324 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -391,11 +391,19 @@ Please retry in a few minutes."; "Scene.Notification.Policy.NotFollowing.Title" = "People you don't follow"; "Scene.Notification.Policy.PrivateMentions.Subtitle" = "Filtered unless it’s in reply to your own mention or if you follow the sender"; "Scene.Notification.Policy.PrivateMentions.Title" = "Unsolicited private mentions"; +"Scene.Notification.Policy.ModeratedAccounts.Title" = "Moderated accounts"; +"Scene.Notification.Policy.ModeratedAccounts.Subtitle" = "Limited by server moderators"; +"Scene.Notification.Policy.Action.Accept.Title" = "Accept"; +"Scene.Notification.Policy.Action.Accept.Subtitle" = "Show in notifications"; +"Scene.Notification.Policy.Action.Filter.Title" = "Filter"; +"Scene.Notification.Policy.Action.Filter.Subtitle" = "Send to filtered notifications inbox"; +"Scene.Notification.Policy.Action.Drop.Title" = "Ignore"; +"Scene.Notification.Policy.Action.Drop.Subtitle" = "Send to the void, never to be seen again"; "Scene.Notification.Policy.Title" = "Filter Notifications from…"; "Scene.Notification.AdminFilter.Reports.Title" = "Admin reports"; -"Scene.Notification.AdminFilter.Reports.Subtitle" = "Hide reports of spam, rule violations, and other complaints"; +"Scene.Notification.AdminFilter.Reports.Subtitle" = "Show reports of spam, rule violations, and other complaints"; "Scene.Notification.AdminFilter.Signups.Title" = "Account signups"; -"Scene.Notification.AdminFilter.Signups.Subtitle" = "Hide notifications of new accounts created on this instance"; +"Scene.Notification.AdminFilter.Signups.Subtitle" = "Show notifications of new accounts created on this instance"; "Scene.Notification.Title.Everything" = "Everything"; "Scene.Notification.Title.Mentions" = "Mentions"; "Scene.Notification.Warning.DeleteStatuses" = "Some of your posts have been removed."; diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift index 85cee122a..e844ca0a9 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift @@ -9,18 +9,25 @@ import Combine import Foundation extension Mastodon.API.Notifications { - internal static func notificationsEndpointURL(domain: String, grouped: Bool = false) -> URL { + internal static func notificationsEndpointURL( + domain: String, grouped: Bool = false + ) -> URL { if grouped { - Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("notifications") + Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent( + "notifications") } else { - Mastodon.API.endpointURL(domain: domain).appendingPathComponent("notifications") + Mastodon.API.endpointURL(domain: domain).appendingPathComponent( + "notifications") } } - internal static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL { - notificationsEndpointURL(domain: domain).appendingPathComponent(notificationID) + internal static func getNotificationEndpointURL( + domain: String, notificationID: String + ) -> URL { + notificationsEndpointURL(domain: domain).appendingPathComponent( + notificationID) } - + /// Get all grouped notifications /// /// - Since: 4.3.0 @@ -47,10 +54,12 @@ extension Mastodon.API.Notifications { authorization: authorization ) let (data, response) = try await session.data(for: request) - let value = try Mastodon.API.decode(type: Mastodon.Entity.GroupedNotificationsResults.self, from: data, response: response) + let value = try Mastodon.API.decode( + type: Mastodon.Entity.GroupedNotificationsResults.self, from: data, + response: response) return value } - + /// Get all notifications /// /// - Since: 0.0.0 @@ -70,7 +79,9 @@ extension Mastodon.API.Notifications { domain: String, query: Mastodon.API.Notifications.Query, authorization: Mastodon.API.OAuth.Authorization - ) -> AnyPublisher, Error> { + ) -> AnyPublisher< + Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error + > { let request = Mastodon.API.get( url: notificationsEndpointURL(domain: domain, grouped: false), query: query, @@ -78,12 +89,15 @@ extension Mastodon.API.Notifications { ) return session.dataTaskPublisher(for: request) .tryMap { data, response in - let value = try Mastodon.API.decode(type: [Mastodon.Entity.Notification].self, from: data, response: response) - return Mastodon.Response.Content(value: value, response: response) + let value = try Mastodon.API.decode( + type: [Mastodon.Entity.Notification].self, from: data, + response: response) + return Mastodon.Response.Content( + value: value, response: response) } .eraseToAnyPublisher() } - + /// Get a single notification /// /// - Since: 0.0.0 @@ -103,16 +117,22 @@ extension Mastodon.API.Notifications { domain: String, notificationID: Mastodon.Entity.Notification.ID, authorization: Mastodon.API.OAuth.Authorization - ) -> AnyPublisher, Error> { + ) -> AnyPublisher< + Mastodon.Response.Content, Error + > { let request = Mastodon.API.get( - url: getNotificationEndpointURL(domain: domain, notificationID: notificationID), + url: getNotificationEndpointURL( + domain: domain, notificationID: notificationID), query: nil, authorization: authorization ) return session.dataTaskPublisher(for: request) .tryMap { data, response in - let value = try Mastodon.API.decode(type: Mastodon.Entity.Notification.self, from: data, response: response) - return Mastodon.Response.Content(value: value, response: response) + let value = try Mastodon.API.decode( + type: Mastodon.Entity.Notification.self, from: data, + response: response) + return Mastodon.Response.Content( + value: value, response: response) } .eraseToAnyPublisher() } @@ -127,7 +147,7 @@ extension Mastodon.API.Notifications { public let types: [Mastodon.Entity.NotificationType]? public let excludeTypes: [Mastodon.Entity.NotificationType]? public let accountID: String? - + public init( maxID: Mastodon.Entity.Status.ID? = nil, sinceID: Mastodon.Entity.Status.ID? = nil, @@ -145,29 +165,42 @@ extension Mastodon.API.Notifications { self.excludeTypes = excludeTypes self.accountID = accountID } - + var queryItems: [URLQueryItem]? { var items: [URLQueryItem] = [] - maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) } - sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) } - minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) } - limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } + maxID.flatMap { + items.append(URLQueryItem(name: "max_id", value: $0)) + } + sinceID.flatMap { + items.append(URLQueryItem(name: "since_id", value: $0)) + } + minID.flatMap { + items.append(URLQueryItem(name: "min_id", value: $0)) + } + limit.flatMap { + items.append(URLQueryItem(name: "limit", value: String($0))) + } if let types = types { types.forEach { - items.append(URLQueryItem(name: "types[]", value: $0.rawValue)) + items.append( + URLQueryItem(name: "types[]", value: $0.rawValue)) } } if let excludeTypes = excludeTypes { excludeTypes.forEach { - items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue)) + items.append( + URLQueryItem( + name: "exclude_types[]", value: $0.rawValue)) } } - accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) } + accountID.flatMap { + items.append(URLQueryItem(name: "account_id", value: $0)) + } guard !items.isEmpty else { return nil } return items } } - + public struct GroupedQuery: PagedQueryType, GetQuery { public let maxID: Mastodon.Entity.Status.ID? public let sinceID: Mastodon.Entity.Status.ID? @@ -178,7 +211,7 @@ extension Mastodon.API.Notifications { public let accountID: String? public let groupedTypes: [String]? public let expandAccounts: Bool - + public init( maxID: Mastodon.Entity.Status.ID? = nil, sinceID: Mastodon.Entity.Status.ID? = nil, @@ -200,29 +233,45 @@ extension Mastodon.API.Notifications { self.groupedTypes = groupedTypes self.expandAccounts = expandAccounts } - + var queryItems: [URLQueryItem]? { var items: [URLQueryItem] = [] - maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) } - sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) } - minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) } - limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } + maxID.flatMap { + items.append(URLQueryItem(name: "max_id", value: $0)) + } + sinceID.flatMap { + items.append(URLQueryItem(name: "since_id", value: $0)) + } + minID.flatMap { + items.append(URLQueryItem(name: "min_id", value: $0)) + } + limit.flatMap { + items.append(URLQueryItem(name: "limit", value: String($0))) + } if let types = types { types.forEach { - items.append(URLQueryItem(name: "types[]", value: $0.rawValue)) + items.append( + URLQueryItem(name: "types[]", value: $0.rawValue)) } } if let excludeTypes = excludeTypes { excludeTypes.forEach { - items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue)) + items.append( + URLQueryItem( + name: "exclude_types[]", value: $0.rawValue)) } } - accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) } + accountID.flatMap { + items.append(URLQueryItem(name: "account_id", value: $0)) + } // TODO: implement groupedTypes -// if let groupedTypes { -// items.append(URLQueryItem(name: "grouped_types", value: groupedTypes)) -// } - items.append(URLQueryItem(name: "expand_accounts", value: expandAccounts ? "full" : "partial_avatars")) + // if let groupedTypes { + // items.append(URLQueryItem(name: "grouped_types", value: groupedTypes)) + // } + items.append( + URLQueryItem( + name: "expand_accounts", + value: expandAccounts ? "full" : "partial_avatars")) guard !items.isEmpty else { return nil } return items } @@ -233,27 +282,48 @@ extension Mastodon.API.Notifications { extension Mastodon.API.Notifications { internal static func notificationPolicyEndpointURL(domain: String) -> URL { - notificationsEndpointURL(domain: domain).appendingPathComponent("policy") + return Mastodon.API.endpointV2URL(domain: domain) + .appendingPathComponent("notifications").appendingPathComponent( + "policy") } public struct UpdateNotificationPolicyQuery: Codable, PatchQuery { - public let filterNotFollowing: Bool - public let filterNotFollowers: Bool - public let filterNewAccounts: Bool - public let filterPrivateMentions: Bool + public let forNotFollowing: + Mastodon.Entity.NotificationPolicy.NotificationFilterAction + public let forNotFollowers: + Mastodon.Entity.NotificationPolicy.NotificationFilterAction + public let forNewAccounts: + Mastodon.Entity.NotificationPolicy.NotificationFilterAction + public let forPrivateMentions: + Mastodon.Entity.NotificationPolicy.NotificationFilterAction + public let forLimitedAccounts: + Mastodon.Entity.NotificationPolicy.NotificationFilterAction enum CodingKeys: String, CodingKey { - case filterNotFollowing = "filter_not_following" - case filterNotFollowers = "filter_not_followers" - case filterNewAccounts = "filter_new_accounts" - case filterPrivateMentions = "filter_private_mentions" + case forNotFollowing = "for_not_following" + case forNotFollowers = "for_not_followers" + case forNewAccounts = "for_new_accounts" + case forPrivateMentions = "for_private_mentions" + case forLimitedAccounts = "for_Limited_accounts" } - public init(filterNotFollowing: Bool, filterNotFollowers: Bool, filterNewAccounts: Bool, filterPrivateMentions: Bool) { - self.filterNotFollowing = filterNotFollowing - self.filterNotFollowers = filterNotFollowers - self.filterNewAccounts = filterNewAccounts - self.filterPrivateMentions = filterPrivateMentions + public init( + forNotFollowing: Mastodon.Entity.NotificationPolicy + .NotificationFilterAction, + forNotFollowers: Mastodon.Entity.NotificationPolicy + .NotificationFilterAction, + forNewAccounts: Mastodon.Entity.NotificationPolicy + .NotificationFilterAction, + forPrivateMentions: Mastodon.Entity.NotificationPolicy + .NotificationFilterAction, + forLimitedAccounts: Mastodon.Entity.NotificationPolicy + .NotificationFilterAction + ) { + self.forNotFollowing = forNotFollowing + self.forNotFollowers = forNotFollowers + self.forNewAccounts = forNewAccounts + self.forPrivateMentions = forPrivateMentions + self.forLimitedAccounts = forLimitedAccounts } } @@ -261,7 +331,9 @@ extension Mastodon.API.Notifications { session: URLSession, domain: String, authorization: Mastodon.API.OAuth.Authorization - ) async throws -> Mastodon.Response.Content { + ) async throws + -> Mastodon.Response.Content + { let request = Mastodon.API.get( url: notificationPolicyEndpointURL(domain: domain), authorization: authorization @@ -269,7 +341,9 @@ extension Mastodon.API.Notifications { let (data, response) = try await session.data(for: request) - let value = try Mastodon.API.decode(type: Mastodon.Entity.NotificationPolicy.self, from: data, response: response) + let value = try Mastodon.API.decode( + type: Mastodon.Entity.NotificationPolicy.self, from: data, + response: response) return Mastodon.Response.Content(value: value, response: response) } @@ -278,41 +352,58 @@ extension Mastodon.API.Notifications { domain: String, authorization: Mastodon.API.OAuth.Authorization, query: Mastodon.API.Notifications.UpdateNotificationPolicyQuery - ) async throws -> Mastodon.Response.Content { + ) async throws + -> Mastodon.Response.Content + { let request = Mastodon.API.patch( url: notificationPolicyEndpointURL(domain: domain), query: query, authorization: authorization ) let (data, response) = try await session.data(for: request) - let value = try Mastodon.API.decode(type: Mastodon.Entity.NotificationPolicy.self, from: data, response: response) + let value = try Mastodon.API.decode( + type: Mastodon.Entity.NotificationPolicy.self, from: data, + response: response) return Mastodon.Response.Content(value: value, response: response) } } extension Mastodon.API.Notifications { - internal static func notificationRequestsEndpointURL(domain: String) -> URL { - notificationsEndpointURL(domain: domain).appendingPathComponent("requests") + internal static func notificationRequestsEndpointURL(domain: String) -> URL + { + notificationsEndpointURL(domain: domain).appendingPathComponent( + "requests") } - internal static func notificationRequestEndpointURL(domain: String, id: String) -> URL { - notificationRequestsEndpointURL(domain: domain).appendingPathComponent(id) + internal static func notificationRequestEndpointURL( + domain: String, id: String + ) -> URL { + notificationRequestsEndpointURL(domain: domain).appendingPathComponent( + id) } - internal static func acceptNotificationRequestEndpointURL(domain: String, id: String) -> URL { - notificationRequestEndpointURL(domain: domain, id: id).appendingPathComponent("accept") + internal static func acceptNotificationRequestEndpointURL( + domain: String, id: String + ) -> URL { + notificationRequestEndpointURL(domain: domain, id: id) + .appendingPathComponent("accept") } - internal static func dismissNotificationRequestEndpointURL(domain: String, id: String) -> URL { - notificationRequestEndpointURL(domain: domain, id: id).appendingPathComponent("dismiss") + internal static func dismissNotificationRequestEndpointURL( + domain: String, id: String + ) -> URL { + notificationRequestEndpointURL(domain: domain, id: id) + .appendingPathComponent("dismiss") } public static func getNotificationRequests( session: URLSession, domain: String, authorization: Mastodon.API.OAuth.Authorization - ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.NotificationRequest]> { + ) async throws + -> Mastodon.Response.Content<[Mastodon.Entity.NotificationRequest]> + { let request = Mastodon.API.get( url: notificationRequestsEndpointURL(domain: domain), authorization: authorization @@ -320,7 +411,9 @@ extension Mastodon.API.Notifications { let (data, response) = try await session.data(for: request) - let value = try Mastodon.API.decode(type: [Mastodon.Entity.NotificationRequest].self, from: data, response: response) + let value = try Mastodon.API.decode( + type: [Mastodon.Entity.NotificationRequest].self, from: data, + response: response) return Mastodon.Response.Content(value: value, response: response) } @@ -338,7 +431,8 @@ extension Mastodon.API.Notifications { let (data, response) = try await session.data(for: request) // we expect an empty dictionary - let value = try Mastodon.API.decode(type: [String: String].self, from: data, response: response) + let value = try Mastodon.API.decode( + type: [String: String].self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } @@ -356,7 +450,8 @@ extension Mastodon.API.Notifications { let (data, response) = try await session.data(for: request) // we expect an empty dictionary - let value = try Mastodon.API.decode(type: [String: String].self, from: data, response: response) + let value = try Mastodon.API.decode( + type: [String: String].self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift index 68755e9b6..a2adca013 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift @@ -4,28 +4,57 @@ import Foundation extension Mastodon.Entity { public struct NotificationPolicy: Codable, Hashable { - public let filterNotFollowing: Bool - public let filterNotFollowers: Bool - public let filterNewAccounts: Bool - public let filterPrivateMentions: Bool + public let forNotFollowing: NotificationFilterAction + public let forNotFollowers: NotificationFilterAction + public let forNewAccounts: NotificationFilterAction + public let forPrivateMentions: NotificationFilterAction + public let forLimitedAccounts: NotificationFilterAction public let summary: Summary - + enum CodingKeys: String, CodingKey { - case filterNotFollowing = "filter_not_following" - case filterNotFollowers = "filter_not_followers" - case filterNewAccounts = "filter_new_accounts" - case filterPrivateMentions = "filter_private_mentions" + case forNotFollowing = "for_not_following" + case forNotFollowers = "for_not_followers" + case forNewAccounts = "for_new_accounts" + case forPrivateMentions = "for_private_mentions" + case forLimitedAccounts = "for_limited_accounts" case summary } - + public struct Summary: Codable, Hashable { public let pendingRequestsCount: Int public let pendingNotificationsCount: Int - + enum CodingKeys: String, CodingKey { case pendingRequestsCount = "pending_requests_count" case pendingNotificationsCount = "pending_notifications_count" } } + + public enum NotificationFilterAction: RawRepresentable, Codable, Sendable, Equatable, Hashable { + public typealias RawValue = String + + case accept + case filter + case drop + case _other(String) + + public init?(rawValue: String) { + switch rawValue { + case "accept": self = .accept + case "filter": self = .filter + case "drop": self = .drop + default: self = ._other(rawValue) + } + } + + public var rawValue: RawValue { + switch self { + case .accept: return "accept" + case .filter: return "filter" + case .drop: return "drop" + case ._other(let string): return string + } + } + } } }