diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 426914cb3..8db2635d6 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -170,6 +170,7 @@ D85DF9742C481B3500A01408 /* DataSourceFacade+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9732C481B3500A01408 /* DataSourceFacade+Notifications.swift */; }; D85DF9762C4965A900A01408 /* NotificationRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9752C4965A900A01408 /* NotificationRequestsViewModel.swift */; }; D85DF97A2C4E49A400A01408 /* NotificationRequestCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9792C4E49A400A01408 /* NotificationRequestCountView.swift */; }; + D85DF97E2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */; }; D87364F92AE28DB500C8F919 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = D87364F82AE28DB500C8F919 /* Kanna */; }; D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; }; @@ -817,6 +818,7 @@ D85DF9732C481B3500A01408 /* DataSourceFacade+Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Notifications.swift"; sourceTree = ""; }; D85DF9752C4965A900A01408 /* NotificationRequestsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestsViewModel.swift; sourceTree = ""; }; D85DF9792C4E49A400A01408 /* NotificationRequestCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestCountView.swift; sourceTree = ""; }; + D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNotificationTimelineViewController.swift; sourceTree = ""; }; D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = ""; }; D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewModel.swift; sourceTree = ""; }; D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginServerTableViewCell.swift; sourceTree = ""; }; @@ -1852,6 +1854,7 @@ D85DF9702C481B1100A01408 /* Requests */ = { isa = PBXGroup; children = ( + D85DF97C2C50EF8700A01408 /* Account Notifications */, D85DF96E2C481B1100A01408 /* NotificationRequestsTableViewController.swift */, D85DF96F2C481B1100A01408 /* NotificationRequestTableViewCell.swift */, D85DF9752C4965A900A01408 /* NotificationRequestsViewModel.swift */, @@ -1861,6 +1864,14 @@ path = "Notification Filtering/Requests"; sourceTree = ""; }; + D85DF97C2C50EF8700A01408 /* Account Notifications */ = { + isa = PBXGroup; + children = ( + D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */, + ); + path = "Account Notifications"; + sourceTree = ""; + }; D8A6AB68291C50F3003AB663 /* Login */ = { isa = PBXGroup; children = ( @@ -3816,6 +3827,7 @@ DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */, 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */, DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */, + D85DF97E2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 3f4f8626f..4ca70d8ec 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -209,7 +209,7 @@ extension SceneCoordinator { // Notifications case notificationPolicy(viewModel: NotificationFilterViewModel) case notificationRequests(viewModel: NotificationRequestsViewModel) - case notificationTimeline(viewModel: NotificationTimelineViewModel) + case accountNotificationTimeline(viewModel: NotificationTimelineViewModel, request: Mastodon.Entity.NotificationRequest) // report case report(viewModel: ReportViewModel) @@ -567,8 +567,8 @@ private extension SceneCoordinator { viewController = NotificationRequestsTableViewController(viewModel: viewModel) case .notificationPolicy(let viewModel): viewController = NotificationPolicyViewController(viewModel: viewModel) - case .notificationTimeline(let viewModel): - viewController = NotificationTimelineViewController(viewModel: viewModel, context: appContext, coordinator: self) + case .accountNotificationTimeline(let viewModel, let request): + viewController = AccountNotificationTimelineViewController(viewModel: viewModel, context: appContext, coordinator: self, notificationRequest: request) } setupDependency(for: viewController as? NeedsDependency) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Notifications.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Notifications.swift index 15ef9f1e5..f6dac507c 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Notifications.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Notifications.swift @@ -28,13 +28,17 @@ extension DataSourceFacade { static func coordinateToNotificationRequest( request: Mastodon.Entity.NotificationRequest, provider: ViewControllerWithDependencies & AuthContextProvider - ) async { + ) async -> AccountNotificationTimelineViewController? { provider.coordinator.showLoading() let notificationTimelineViewModel = NotificationTimelineViewModel(context: provider.context, authContext: provider.authContext, scope: .fromAccount(request.account)) provider.coordinator.hideLoading() - provider.coordinator.present(scene: .notificationTimeline(viewModel: notificationTimelineViewModel), transition: .show) + + guard let viewController = provider.coordinator.present(scene: .accountNotificationTimeline(viewModel: notificationTimelineViewModel, request: request), transition: .show) as? AccountNotificationTimelineViewController else { return nil } + + return viewController + } } diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/Account Notifications/AccountNotificationTimelineViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/Account Notifications/AccountNotificationTimelineViewController.swift new file mode 100644 index 000000000..0118c25e2 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/Account Notifications/AccountNotificationTimelineViewController.swift @@ -0,0 +1,54 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonCore +import MastodonSDK + +protocol AccountNotificationTimelineViewControllerDelegate: AnyObject { + func acceptRequest(_ viewController: AccountNotificationTimelineViewController, request: Mastodon.Entity.NotificationRequest, completion: @escaping (() -> Void)) + func dismissRequest(_ viewController: AccountNotificationTimelineViewController, request: Mastodon.Entity.NotificationRequest, completion: @escaping (() -> Void)) +} + +class AccountNotificationTimelineViewController: NotificationTimelineViewController { + + let request: Mastodon.Entity.NotificationRequest + weak var delegate: AccountNotificationTimelineViewControllerDelegate? + + init(viewModel: NotificationTimelineViewModel, context: AppContext, coordinator: SceneCoordinator, notificationRequest: Mastodon.Entity.NotificationRequest) { + self.request = notificationRequest + + super.init(viewModel: viewModel, context: context, coordinator: coordinator) + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: nil, image: UIImage(systemName: "ellipsis.circle"), target: nil, action: nil, menu: menu()) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MARK: - Actions + + //TODO: Localization + func menu() -> UIMenu { + let menu = UIMenu(children: [ + UIAction(title: "Accept", image: UIImage(systemName: "checkmark")) { [weak self] _ in + guard let self else { return } + + coordinator.showLoading() + self.delegate?.acceptRequest(self, request: request) { + self.navigationController?.popViewController(animated: true) + } + coordinator.hideLoading() + }, + UIAction(title: "Dismiss", image: UIImage(systemName: "speaker.slash")) { [weak self] _ in + guard let self else { return } + + coordinator.showLoading() + self.delegate?.dismissRequest(self, request: request) { + self.navigationController?.popViewController(animated: true) + } + coordinator.hideLoading() + } + ]) + + return menu + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestTableViewCell.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestTableViewCell.swift index 0bb49f7f2..95b23e506 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestTableViewCell.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestTableViewCell.swift @@ -57,6 +57,7 @@ class NotificationRequestTableViewCell: UITableViewCell { acceptNotificationRequestButton.translatesAutoresizingMaskIntoConstraints = false acceptNotificationRequestButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold) acceptNotificationRequestButton.setTitleColor(.white, for: .normal) + //TODO: Localization acceptNotificationRequestButton.setTitle("Accept", for: .normal) acceptNotificationRequestButton.setImage(UIImage(systemName: "checkmark"), for: .normal) acceptNotificationRequestButton.imageView?.contentMode = .scaleAspectFit @@ -82,6 +83,7 @@ class NotificationRequestTableViewCell: UITableViewCell { rejectNotificationRequestButton.translatesAutoresizingMaskIntoConstraints = false rejectNotificationRequestButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold) rejectNotificationRequestButton.setTitleColor(.black, for: .normal) + //TODO: Localization rejectNotificationRequestButton.setTitle("Dismiss", for: .normal) rejectNotificationRequestButton.setImage(UIImage(systemName: "speaker.slash"), for: .normal) rejectNotificationRequestButton.imageView?.contentMode = .scaleAspectFit diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsTableViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsTableViewController.swift index fd216279f..597535aeb 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsTableViewController.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsTableViewController.swift @@ -4,6 +4,7 @@ import UIKit import MastodonSDK import MastodonCore import MastodonAsset +import MastodonLocalization enum NotificationRequestsSection: Hashable { case main @@ -58,8 +59,7 @@ class NotificationRequestsTableViewController: UIViewController, NeedsDependency tableView.delegate = self self.dataSource = dataSource - //TODO: Localization - title = "Filtered Notifications" + title = L10n.Scene.Notification.FilteredNotificationBanner.title } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -81,8 +81,11 @@ extension NotificationRequestsTableViewController: UITableViewDelegate { let request = viewModel.requests[indexPath.row] - Task { - await DataSourceFacade.coordinateToNotificationRequest(request: request, provider: self) + Task { [weak self] in + guard let self else { return } + + let viewController = await DataSourceFacade.coordinateToNotificationRequest(request: request, provider: self) + viewController?.delegate = self } } @@ -167,30 +170,7 @@ extension NotificationRequestsTableViewController: NotificationRequestTableViewC Task { [weak self] in guard let self else { return } do { - _ = try await context.apiService.rejectNotificationRequests(authenticationBox: authContext.mastodonAuthenticationBox, - id: notificationRequest.id) - - let requests = try await context.apiService.notificationRequests(authenticationBox: authContext.mastodonAuthenticationBox).value - - NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil) - - if requests.count > 0 { - - await MainActor.run { [weak self] in - guard let self else { return } - self.viewModel.requests = requests - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(self.viewModel.requests.compactMap { NotificationRequestItem.item($0) } ) - - self.dataSource?.apply(snapshot) - } - } else { - await MainActor.run { [weak self] in - _ = self?.navigationController?.popViewController(animated: true) - } - } - + try await rejectNotificationRequest(notificationRequest) } catch { cell.rejectNotificationRequestActivityIndicatorView.stopAnimating() cell.rejectNotificationRequestButton.tintColor = .black @@ -200,4 +180,46 @@ extension NotificationRequestsTableViewController: NotificationRequestTableViewC } } } + + private func rejectNotificationRequest(_ notificationRequest: MastodonSDK.Mastodon.Entity.NotificationRequest) async throws { + _ = try await context.apiService.rejectNotificationRequests(authenticationBox: authContext.mastodonAuthenticationBox, + id: notificationRequest.id) + + let requests = try await context.apiService.notificationRequests(authenticationBox: authContext.mastodonAuthenticationBox).value + + NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil) + + if requests.count > 0 { + await MainActor.run { [weak self] in + guard let self else { return } + self.viewModel.requests = requests + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(self.viewModel.requests.compactMap { NotificationRequestItem.item($0) } ) + + self.dataSource?.apply(snapshot) + } + } else { + await MainActor.run { [weak self] in + _ = self?.navigationController?.popViewController(animated: true) + } + } + + } +} + +extension NotificationRequestsTableViewController: AccountNotificationTimelineViewControllerDelegate { + func acceptRequest(_ viewController: AccountNotificationTimelineViewController, request: MastodonSDK.Mastodon.Entity.NotificationRequest, completion: @escaping (() -> Void)) { + Task { + try? await rejectNotificationRequest(request) + completion() + } + } + + func dismissRequest(_ viewController: AccountNotificationTimelineViewController, request: MastodonSDK.Mastodon.Entity.NotificationRequest, completion: @escaping (() -> Void)) { + Task { + try? await rejectNotificationRequest(request) + completion() + } + } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 5a8f77349..f25907070 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -11,7 +11,7 @@ import CoreDataStack import MastodonCore import MastodonLocalization -final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { +class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { weak var context: AppContext! weak var coordinator: SceneCoordinator!