2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

Update to three-way choice API for notification filtering

This commit is contained in:
shannon 2025-04-04 13:49:12 -04:00
parent c9da109ea3
commit fa49cd6630
11 changed files with 592 additions and 190 deletions

View File

@ -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 its 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"
}
}
},

View File

@ -826,16 +826,34 @@
"private_mentions": {
"title": "Unsolicited private mentions",
"subtitle": "Filtered unless its 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"
}
}
},

View File

@ -70,16 +70,17 @@ class NotificationListViewController: UIHostingController<NotificationListView>
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
)

View File

@ -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 ""
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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<Mastodon.Entity.NotificationPolicy> {
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,

View File

@ -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")

View File

@ -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 its 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.";

View File

@ -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<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, 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<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
) -> AnyPublisher<
Mastodon.Response.Content<Mastodon.Entity.Notification>, 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<Mastodon.Entity.NotificationPolicy> {
) async throws
-> Mastodon.Response.Content<Mastodon.Entity.NotificationPolicy>
{
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<Mastodon.Entity.NotificationPolicy> {
) async throws
-> Mastodon.Response.Content<Mastodon.Entity.NotificationPolicy>
{
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)
}
}

View File

@ -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
}
}
}
}
}