diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index ac5edf1ac..3be531abf 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -765,6 +765,7 @@ "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,6 +828,16 @@ "title": "Unsolicited private mentions", "subtitle": "Filtered unless it’s in reply to your own mention or if you follow the sender" } + }, + "admin_filter": { + "reports": { + "title": "Admin reports", + "subtitle": "Hide 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" + } } }, "thread": { diff --git a/Localization/app.json b/Localization/app.json index ac5edf1ac..c5d2ece30 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -827,6 +827,16 @@ "title": "Unsolicited private mentions", "subtitle": "Filtered unless it’s in reply to your own mention or if you follow the sender" } + }, + "admin_filter": { + "reports": { + "title": "Admin reports", + "subtitle": "Hide 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" + } } }, "thread": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 14ab390c5..e03fe33d8 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -513,6 +513,7 @@ FB7C4CCC2CD55DEB00F6129A /* NavigationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7C4CCB2CD55DEB00F6129A /* NavigationFlow.swift */; }; FB7C4CCE2CD55DFF00F6129A /* NewDonationNavigationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7C4CCD2CD55DFE00F6129A /* NewDonationNavigationFlow.swift */; }; FB91184C2D9EB96E003F410B /* Bodega in Frameworks */ = {isa = PBXBuildFile; productRef = FB91184B2D9EB96E003F410B /* Bodega */; }; + FB91188D2D9EECE7003F410B /* BodegaPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB91188C2D9EC208003F410B /* BodegaPersistence.swift */; }; FBD689B52CCBF0AC00CE29F3 /* DonationCampaignViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD689B42CCBF09F00CE29F3 /* DonationCampaignViewModel.swift */; }; /* End PBXBuildFile section */ @@ -1269,6 +1270,7 @@ FB8522712CEE302300BA2757 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/WidgetExtension.strings; sourceTree = ""; }; FB8522722CEE302300BA2757 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; FB8522732CEE302300BA2757 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Intents.stringsdict; sourceTree = ""; }; + FB91188C2D9EC208003F410B /* BodegaPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodegaPersistence.swift; sourceTree = ""; }; FBD689B42CCBF09F00CE29F3 /* DonationCampaignViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationCampaignViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2214,6 +2216,7 @@ DB427DD425BAA00100D1B89D /* Mastodon */ = { isa = PBXGroup; children = ( + FB91188C2D9EC208003F410B /* BodegaPersistence.swift */, FBBEA04E2D380FC70000A900 /* In Progress New Layout and Datamodel */, DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB427DE325BAA00100D1B89D /* Info.plist */, @@ -3818,6 +3821,7 @@ 2A4C3B5D2C579CFB008DD42B /* NotificationRequestConstants.swift in Sources */, DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */, DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */, + FB91188D2D9EECE7003F410B /* BodegaPersistence.swift in Sources */, DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */, DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */, D8F9170B2A4B2C80008A5370 /* AboutSettings.swift in Sources */, diff --git a/Mastodon/BodegaPersistence.swift b/Mastodon/BodegaPersistence.swift new file mode 100644 index 000000000..5b6756c0a --- /dev/null +++ b/Mastodon/BodegaPersistence.swift @@ -0,0 +1,24 @@ +// Copyright © 2025 Mastodon gGmbH. All rights reserved. + +import Bodega +import MastodonCore + +/// Cache user data in a local database. +/// MAKE SURE TO UPDATE removeUser() WHEN ADDING ADDITIONAL CACHES +public class BodegaPersistence { + private static let adminNotificationPreferenceStore = ObjectStorage(storage: SQLiteStorageEngine(directory: .documents(appendingPath: "AdminNotificationPreferences"))!) + + public static func removeUser(_ userID: UserIdentifier) async throws { + try await adminNotificationPreferenceStore.removeObject(forKey: CacheKey(userID.globallyUniqueUserIdentifier)) + } + + public struct Notifications { + static func currentPreferences(for userID: UserIdentifier) async -> AdminNotificationFilterSettings? { + return await adminNotificationPreferenceStore.object(forKey: CacheKey(userID.globallyUniqueUserIdentifier)) + } + + static func updatePreferences(_ preferences: AdminNotificationFilterSettings, for userID: UserIdentifier) async throws { + try await adminNotificationPreferenceStore.store(preferences, forKey: CacheKey(userID.globallyUniqueUserIdentifier)) + } + } +} diff --git a/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift b/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift index 1bcdc17b3..2c4eec0c4 100644 --- a/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift +++ b/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift @@ -344,8 +344,9 @@ extension GroupedNotificationFeedLoader { .currentActiveUser.value else { throw APIService.APIError.implicit(.authenticationMissing) } + let adminFilterPreferences = await BodegaPersistence.Notifications.currentPreferences(for: authenticationBox) let results = try await APIService.shared.groupedNotifications( - olderThan: maxID, newerThan: minID, fromAccount: nil, scope: scope, + olderThan: maxID, newerThan: minID, fromAccount: nil, scope: scope, excludingAdminTypes: adminFilterPreferences?.excludedNotificationTypes, authenticationBox: authenticationBox ) diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift index 202448105..68184fee5 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift @@ -62,11 +62,25 @@ class NotificationListViewController: UIHostingController @objc private func showNotificationPolicySettings(_ sender: Any) { guard let policy = viewModel.filteredNotificationsViewModel.policy else { return } Task { + let adminSettings: AdminNotificationFilterSettings? = await { + guard let user = AuthenticationServiceProvider.shared.currentActiveUser.value, let role = user.cachedAccount?.role else { print("no role"); return nil } + let hasAdminPermissions = role.hasPermissions(.administrator) || role.hasPermissions(.manageReports) || role.hasPermissions(.manageUsers) + guard hasAdminPermissions else { print("no permissions"); return nil } + if let existingPreferences = await BodegaPersistence.Notifications.currentPreferences(for: user.authentication) { + return existingPreferences + } else { + return AdminNotificationFilterSettings(filterOutReports: false, filterOutSignups: false) + } + }() + let policyViewModel = await NotificationFilterViewModel( - notFollowing: policy.filterNotFollowing, - noFollower: policy.filterNotFollowers, - newAccount: policy.filterNewAccounts, - privateMentions: policy.filterPrivateMentions + NotificationFilterSettings( + notFollowing: policy.filterNotFollowing, + noFollower: policy.filterNotFollowers, + newAccount: policy.filterNewAccounts, + privateMentions: policy.filterPrivateMentions + ), + adminSettings: adminSettings ) guard let policyViewController = self.sceneCoordinator?.present(scene: .notificationPolicy(viewModel: policyViewModel), transition: .formSheet) as? NotificationPolicyViewController else { return } diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift index 76950c213..4b5b38ebe 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift @@ -31,17 +31,7 @@ class NotificationPolicyFilterTableViewCell: ToggleTableViewCell { subtitleLabel.text = filterItem.subtitle self.filterItem = filterItem - let toggleIsOn: Bool - switch filterItem { - case .notFollowing: - toggleIsOn = viewModel.notFollowing - case .noFollower: - toggleIsOn = viewModel.noFollower - case .newAccount: - toggleIsOn = viewModel.newAccount - case .privateMentions: - toggleIsOn = viewModel.privateMentions - } + let toggleIsOn = viewModel.value(forItem: filterItem) toggle.isOn = toggleIsOn } diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift index 7d644f0a4..953de77e7 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift @@ -1,20 +1,23 @@ // Copyright © 2024 Mastodon gGmbH. All rights reserved. -import UIKit -import MastodonLocalization import MastodonAsset import MastodonCore +import MastodonLocalization import MastodonSDK +import UIKit enum NotificationFilterSection: Hashable { case main + case admin } -enum NotificationFilterItem: Hashable, CaseIterable { +enum NotificationFilterItem: Hashable { case notFollowing case noFollower case newAccount case privateMentions + case adminReports + case adminSignups var title: String { switch self { @@ -26,6 +29,10 @@ enum NotificationFilterItem: Hashable, CaseIterable { return L10n.Scene.Notification.Policy.NewAccount.title case .privateMentions: return L10n.Scene.Notification.Policy.PrivateMentions.title + case .adminReports: + return L10n.Scene.Notification.AdminFilter.Reports.title + case .adminSignups: + return L10n.Scene.Notification.AdminFilter.Signups.title } } @@ -39,29 +46,114 @@ enum NotificationFilterItem: Hashable, CaseIterable { return L10n.Scene.Notification.Policy.NewAccount.subtitle case .privateMentions: return L10n.Scene.Notification.Policy.PrivateMentions.subtitle + case .adminReports: + return L10n.Scene.Notification.AdminFilter.Reports.subtitle + case .adminSignups: + return L10n.Scene.Notification.AdminFilter.Signups.subtitle } } } -struct NotificationFilterViewModel { - var notFollowing: Bool - var noFollower: Bool - var newAccount: Bool - var privateMentions: Bool +struct NotificationFilterSettings: Codable, Equatable { + let notFollowing: Bool + let noFollower: Bool + let newAccount: Bool + let privateMentions: Bool +} +struct AdminNotificationFilterSettings: Codable, Equatable { + let filterOutReports: Bool + let filterOutSignups: Bool + + var excludedNotificationTypes: [Mastodon.Entity.NotificationType]? { + var excluded = [Mastodon.Entity.NotificationType]() + if filterOutReports { + excluded.append(.adminReport) + } + if filterOutSignups { + excluded.append(.adminSignUp) + } + return excluded.isEmpty ? nil : excluded + } +} - let appContext: AppContext +class NotificationFilterViewModel { + let originalRegularSettings: NotificationFilterSettings + let originalAdminSettings: AdminNotificationFilterSettings? - init(notFollowing: Bool, noFollower: Bool, newAccount: Bool, privateMentions: Bool) async { - self.notFollowing = notFollowing - self.noFollower = noFollower - self.newAccount = newAccount - self.privateMentions = privateMentions - self.appContext = await AppContext.shared + var regularFilterSettings: NotificationFilterSettings + var adminFilterSettings: AdminNotificationFilterSettings? + + init( + _ regularSettings: NotificationFilterSettings, + adminSettings: AdminNotificationFilterSettings? + ) async { + self.originalRegularSettings = regularSettings + self.regularFilterSettings = regularSettings + self.originalAdminSettings = adminSettings + self.adminFilterSettings = adminSettings + } + + func value(forItem item: NotificationFilterItem) -> Bool { + switch item { + case .notFollowing: + return regularFilterSettings.notFollowing + case .noFollower: + return regularFilterSettings.noFollower + case .newAccount: + return regularFilterSettings.newAccount + case .privateMentions: + return regularFilterSettings.privateMentions + case .adminReports: + return adminFilterSettings?.filterOutReports ?? true + case .adminSignups: + return adminFilterSettings?.filterOutSignups ?? true + } + } + + func setValue(_ value: Bool, forItem item: NotificationFilterItem) { + switch item { + case .notFollowing: + regularFilterSettings = NotificationFilterSettings( + notFollowing: value, + noFollower: regularFilterSettings.noFollower, + newAccount: regularFilterSettings.newAccount, + privateMentions: regularFilterSettings.privateMentions) + case .noFollower: + regularFilterSettings = NotificationFilterSettings( + notFollowing: regularFilterSettings.notFollowing, + noFollower: value, + newAccount: regularFilterSettings.newAccount, + privateMentions: regularFilterSettings.privateMentions) + case .newAccount: + regularFilterSettings = NotificationFilterSettings( + notFollowing: regularFilterSettings.notFollowing, + noFollower: regularFilterSettings.noFollower, + newAccount: value, + privateMentions: regularFilterSettings.privateMentions) + case .privateMentions: + regularFilterSettings = NotificationFilterSettings( + notFollowing: regularFilterSettings.notFollowing, + noFollower: regularFilterSettings.noFollower, + newAccount: regularFilterSettings.newAccount, + privateMentions: value) + case .adminReports: + guard let adminFilterSettings else { return } + self.adminFilterSettings = AdminNotificationFilterSettings( + filterOutReports: value, + filterOutSignups: adminFilterSettings.filterOutSignups) + case .adminSignups: + guard let adminFilterSettings else { return } + self.adminFilterSettings = AdminNotificationFilterSettings( + filterOutReports: adminFilterSettings.filterOutReports, + filterOutSignups: value) + } } } protocol NotificationPolicyViewControllerDelegate: AnyObject { - func policyUpdated(_ viewController: NotificationPolicyViewController, newPolicy: Mastodon.Entity.NotificationPolicy) + func policyUpdated( + _ viewController: NotificationPolicyViewController, + newPolicy: Mastodon.Entity.NotificationPolicy) } class NotificationPolicyViewController: UIViewController { @@ -69,31 +161,57 @@ class NotificationPolicyViewController: UIViewController { let tableView: UITableView let headerBar: NotificationPolicyHeaderView var saveItem: UIBarButtonItem? - var dataSource: UITableViewDiffableDataSource? - let items: [NotificationFilterItem] + var dataSource: + UITableViewDiffableDataSource< + NotificationFilterSection, NotificationFilterItem + >? + let regularItems: [NotificationFilterItem] + let adminItems: [NotificationFilterItem] var viewModel: NotificationFilterViewModel weak var delegate: NotificationPolicyViewControllerDelegate? init(viewModel: NotificationFilterViewModel) { self.viewModel = viewModel - items = NotificationFilterItem.allCases + regularItems = [.notFollowing, .noFollower, .newAccount, .privateMentions] + adminItems = [.adminReports, .adminSignups] headerBar = NotificationPolicyHeaderView() headerBar.translatesAutoresizingMaskIntoConstraints = false tableView = UITableView(frame: .zero, style: .insetGrouped) tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.register(NotificationPolicyFilterTableViewCell.self, forCellReuseIdentifier: NotificationPolicyFilterTableViewCell.reuseIdentifier) + tableView.register( + NotificationPolicyFilterTableViewCell.self, + forCellReuseIdentifier: NotificationPolicyFilterTableViewCell + .reuseIdentifier) tableView.contentInset.top = -20 super.init(nibName: nil, bundle: nil) - let dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in - guard let self, let cell = tableView.dequeueReusableCell(withIdentifier: NotificationPolicyFilterTableViewCell.reuseIdentifier, for: indexPath) as? NotificationPolicyFilterTableViewCell else { + let dataSource = UITableViewDiffableDataSource< + 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 { fatalError("No NotificationPolicyFilterTableViewCell") } - let item = items[indexPath.row] + let item: NotificationFilterItem? + switch indexPath.section { + case 0: + item = regularItems[indexPath.row] + case 1: + item = adminItems[indexPath.row] + default: + item = nil + assertionFailure() + } + guard let item else { return nil } cell.configure(with: item, viewModel: self.viewModel) cell.delegate = self @@ -107,7 +225,9 @@ class NotificationPolicyViewController: UIViewController { view.addSubview(headerBar) view.addSubview(tableView) view.backgroundColor = .systemGroupedBackground - headerBar.closeButton.addTarget(self, action: #selector(NotificationPolicyViewController.save(_:)), for: .touchUpInside) + headerBar.closeButton.addTarget( + self, action: #selector(NotificationPolicyViewController.save(_:)), + for: .touchUpInside) setupConstraints() } @@ -115,15 +235,23 @@ class NotificationPolicyViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot< + NotificationFilterSection, NotificationFilterItem + >() snapshot.appendSections([.main]) - snapshot.appendItems(items) + snapshot.appendItems(regularItems) + if let adminFilterSettings = viewModel.adminFilterSettings { + snapshot.appendSections([.admin]) + snapshot.appendItems(adminItems) + } dataSource?.apply(snapshot, animatingDifferences: false) } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } private func setupConstraints() { let constraints = [ @@ -143,28 +271,39 @@ class NotificationPolicyViewController: UIViewController { // MARK: - Action @objc private func save(_ sender: UIButton) { - guard let authenticationBox = AuthenticationServiceProvider.shared.currentActiveUser.value else { return } + guard + let authenticationBox = AuthenticationServiceProvider.shared + .currentActiveUser.value + else { return } Task { [weak self] in guard let self else { return } do { - let updatedPolicy = try await APIService.shared.updateNotificationPolicy( - authenticationBox: authenticationBox, - filterNotFollowing: viewModel.notFollowing, - filterNotFollowers: viewModel.noFollower, - filterNewAccounts: viewModel.newAccount, - filterPrivateMentions: viewModel.privateMentions - ).value + if let adminPreferences = viewModel.adminFilterSettings, viewModel.adminFilterSettings != viewModel.originalAdminSettings { + try await BodegaPersistence.Notifications.updatePreferences(adminPreferences, for: authenticationBox) + } + } catch {} + + do { + 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) + ).value delegate?.policyUpdated(self, newPolicy: updatedPolicy) - NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil) + NotificationCenter.default.post( + name: .notificationFilteringChanged, object: nil) } catch {} } - dismiss(animated:true) + dismiss(animated: true) } @objc private func cancel(_ sender: UIBarButtonItem) { @@ -175,20 +314,24 @@ class NotificationPolicyViewController: UIViewController { //MARK: - UITableViewDelegate extension NotificationPolicyViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + func tableView( + _ tableView: UITableView, didSelectRowAt indexPath: IndexPath + ) { tableView.deselectRow(at: indexPath, animated: true) - let filterItem = items[indexPath.row] - switch filterItem { - case .notFollowing: - viewModel.notFollowing.toggle() - case .noFollower: - viewModel.noFollower.toggle() - case .newAccount: - viewModel.newAccount.toggle() - case .privateMentions: - viewModel.privateMentions.toggle() - } + let filterItem: NotificationFilterItem? = { + switch indexPath.section { + case 0: + return regularItems[indexPath.row] + case 1: + return adminItems[indexPath.row] + default: + return nil + } + }() + guard let filterItem else { return } + let currentValue = viewModel.value(forItem: filterItem) + viewModel.setValue(!currentValue, forItem: filterItem) if let snapshot = dataSource?.snapshot() { dataSource?.applySnapshotUsingReloadData(snapshot) @@ -196,17 +339,13 @@ extension NotificationPolicyViewController: UITableViewDelegate { } } -extension NotificationPolicyViewController: NotificationPolicyFilterTableViewCellDelegate { - func toggleValueChanged(_ tableViewCell: NotificationPolicyFilterTableViewCell, filterItem: NotificationFilterItem, newValue: Bool) { - switch filterItem { - case .notFollowing: - viewModel.notFollowing = newValue - case .noFollower: - viewModel.noFollower = newValue - case .newAccount: - viewModel.newAccount = newValue - case .privateMentions: - viewModel.privateMentions = newValue - } +extension NotificationPolicyViewController: + NotificationPolicyFilterTableViewCellDelegate +{ + func toggleValueChanged( + _ tableViewCell: NotificationPolicyFilterTableViewCell, + filterItem: NotificationFilterItem, newValue: Bool + ) { + viewModel.setValue(newValue, forItem: filterItem) } } diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index afff32207..4b9de69e1 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -119,16 +119,6 @@ extension NotificationViewController { guard let viewModel, let policy = viewModel.notificationPolicy else { return } Task { - let policyViewModel = await NotificationFilterViewModel( - notFollowing: policy.filterNotFollowing, - noFollower: policy.filterNotFollowers, - newAccount: policy.filterNewAccounts, - privateMentions: policy.filterPrivateMentions - ) - - guard let policyViewController = self.sceneCoordinator?.present(scene: .notificationPolicy(viewModel: policyViewModel), transition: .formSheet) as? NotificationPolicyViewController else { return } - - policyViewController.delegate = self } } } diff --git a/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift b/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift index 222832eac..444de73ab 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift @@ -290,13 +290,13 @@ private extension MastodonFeedLoader { } } - private func _getGroupedNotifications(withScope scope: APIService.MastodonNotificationScope? = nil, accountID: String? = nil, olderThan maxID: String? = nil, newerThan minID: String?) async throws -> [MastodonFeedItemIdentifier] { + private func _getGroupedNotifications(withScope scope: APIService.MastodonNotificationScope? = nil, excludingAdminTypes: [Mastodon.Entity.NotificationType]?, accountID: String? = nil, olderThan maxID: String? = nil, newerThan minID: String?) async throws -> [MastodonFeedItemIdentifier] { assert(scope != nil || accountID != nil, "need a scope or an accountID") guard let authenticationBox = AuthenticationServiceProvider.shared.currentActiveUser.value else { throw APIService.APIError.implicit(.authenticationMissing) } - let results = try await APIService.shared.groupedNotifications(olderThan: maxID, newerThan: minID, fromAccount: accountID, scope: scope, authenticationBox: authenticationBox) + let results = try await APIService.shared.groupedNotifications(olderThan: maxID, newerThan: minID, fromAccount: accountID, scope: scope, excludingAdminTypes: excludingAdminTypes, authenticationBox: authenticationBox) for account in results.accounts { MastodonFeedItemCacheManager.shared.addToCache(account) @@ -319,13 +319,13 @@ private extension MastodonFeedLoader { } } - private func _getGroupedNotificationResults(withScope scope: APIService.MastodonNotificationScope? = nil, accountID: String? = nil, olderThan maxID: String? = nil, newerThan minID: String?) async throws -> Mastodon.Entity.GroupedNotificationsResults { + private func _getGroupedNotificationResults(withScope scope: APIService.MastodonNotificationScope? = nil, excludingAdminTypes: [Mastodon.Entity.NotificationType], accountID: String? = nil, olderThan maxID: String? = nil, newerThan minID: String?) async throws -> Mastodon.Entity.GroupedNotificationsResults { assert(scope != nil || accountID != nil, "need a scope or an accountID") guard let authenticationBox = AuthenticationServiceProvider.shared.currentActiveUser.value else { throw APIService.APIError.implicit(.authenticationMissing) } - let results = try await APIService.shared.groupedNotifications(olderThan: maxID, newerThan: minID, fromAccount: accountID, scope: scope, authenticationBox: authenticationBox) + let results = try await APIService.shared.groupedNotifications(olderThan: maxID, newerThan: minID, fromAccount: accountID, scope: scope, excludingAdminTypes: excludingAdminTypes, authenticationBox: authenticationBox) return results } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 565043e01..f73c0a239 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -63,6 +63,7 @@ extension APIService { newerThan minID: Mastodon.Entity.Notification.ID?, fromAccount accountID: String? = nil, scope: MastodonNotificationScope?, + excludingAdminTypes: [Mastodon.Entity.NotificationType]?, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Entity.GroupedNotificationsResults { let authorization = authenticationBox.userAuthorization @@ -73,10 +74,10 @@ extension APIService { switch scope { case .everything: types = nil - excludedTypes = [.adminReport, .adminSignUp] + excludedTypes = excludingAdminTypes case .mentions: types = [.mention] - excludedTypes = [.follow, .followRequest, .reblog, .favourite, .poll] + excludedTypes = [.follow, .followRequest, .reblog, .favourite, .poll,.adminReport, .adminSignUp] case nil: types = nil excludedTypes = nil diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 362d53ea2..a64c363e2 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -943,6 +943,20 @@ public enum L10n { public static let learnMoreAboutServerBlocks = L10n.tr("Localizable", "Scene.Notification.LearnMoreAboutServerBlocks", fallback: "Learn more about server blocks") /// View report 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") + /// 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") + /// Account signups + public static let title = L10n.tr("Localizable", "Scene.Notification.AdminFilter.Signups.Title", fallback: "Account signups") + } + } public enum FilteredNotification { /// Accept public static let accept = L10n.tr("Localizable", "Scene.Notification.FilteredNotification.Accept", fallback: "Accept") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index f7caab3ce..00f022de8 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -392,6 +392,10 @@ Please retry in a few minutes."; "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.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.Signups.Title" = "Account signups"; +"Scene.Notification.AdminFilter.Signups.Subtitle" = "Hide 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.";