From 484334803412d6cfe8c267a80b8488cbd8a75986 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 25 Jun 2024 12:09:24 +0200 Subject: [PATCH] Add and show filter-banner in "everything"-notifications (if there are any) (IOS-241) --- Mastodon.xcodeproj/project.pbxproj | 4 ++ .../DataSourceFacade+SearchHistory.swift | 44 +++++++------ ...er+NotificationTableViewCellDelegate.swift | 6 +- ...Provider+StatusTableViewCellDelegate.swift | 4 +- ...taSourceProvider+UITableViewDelegate.swift | 61 ++++++++++--------- .../Provider/DataSourceProvider.swift | 1 + ...ficationFilteringBannerTableViewCell.swift | 22 +++++++ .../Scene/Notification/NotificationItem.swift | 1 + .../Notification/NotificationSection.swift | 7 +++ ...ineViewController+DataSourceProvider.swift | 4 +- .../NotificationTimelineViewController.swift | 18 ++++-- ...tificationTimelineViewModel+Diffable.swift | 5 +- .../NotificationTimelineViewModel.swift | 5 +- .../NotificationViewController.swift | 19 +++--- ...ultViewController+DataSourceProvider.swift | 2 +- .../Mastodon+Entity+NotificationPolicy.swift | 18 +++--- 16 files changed, 137 insertions(+), 84 deletions(-) create mode 100644 Mastodon/Scene/Notification/Cell/NotificationFilteringBannerTableViewCell.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3d6f6970f..0743f5b72 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -158,6 +158,7 @@ D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */; }; D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A892A4468DC00C0FB73 /* AboutViewController.swift */; }; D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; }; + D83B54F82C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */; }; D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */; }; D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; }; D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; }; @@ -791,6 +792,7 @@ D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewController.swift; sourceTree = ""; }; D8318A892A4468DC00C0FB73 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = ""; tabWidth = 4; }; + D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFilteringBannerTableViewCell.swift; sourceTree = ""; }; D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusPill.swift; sourceTree = ""; }; D84C099D2B0F9E33009E685E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; D84C099F2B0F9E41009E685E /* Setup.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Setup.md; sourceTree = ""; }; @@ -1512,6 +1514,7 @@ DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */, DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */, D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */, + D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */, ); path = Cell; sourceTree = ""; @@ -3780,6 +3783,7 @@ 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, + D83B54F82C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */, D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 76b19fa10..63ccabbf9 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -17,32 +17,30 @@ extension DataSourceFacade { item: DataSourceItem ) async { switch item { - case .account(account: let account, relationship: _): - let now = Date() - let userID = provider.authContext.mastodonAuthenticationBox.userID - let searchEntry = Persistence.SearchHistory.Item( - updatedAt: now, - userID: userID, - account: account, - hashtag: nil - ) + case .account(account: let account, relationship: _): + let now = Date() + let userID = provider.authContext.mastodonAuthenticationBox.userID + let searchEntry = Persistence.SearchHistory.Item( + updatedAt: now, + userID: userID, + account: account, + hashtag: nil + ) - try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) - case .hashtag(let tag): + try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) + case .hashtag(let tag): - let now = Date() - let userID = provider.authContext.mastodonAuthenticationBox.userID - let searchEntry = Persistence.SearchHistory.Item( - updatedAt: now, - userID: userID, - account: nil, - hashtag: tag - ) + let now = Date() + let userID = provider.authContext.mastodonAuthenticationBox.userID + let searchEntry = Persistence.SearchHistory.Item( + updatedAt: now, + userID: userID, + account: nil, + hashtag: tag + ) - try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) - case .status: - break - case .notification: + try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) + case .status, .notification, .notificationBanner(_): break } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index aa71991d3..5d23083b8 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -514,10 +514,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut ) case .account(let account, _): await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) - case .notification: - assertionFailure("TODO") - case .hashtag(_): - assertionFailure("TODO") + case .notification, .hashtag(_), .notificationBanner(_): + print("TODO") } } // end Task } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 438149890..24ad78b28 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -618,9 +618,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte provider: self, account: account ) - case .notification: - assertionFailure("TODO") - case .hashtag(_): + case .notification, .hashtag(_), .notificationBanner(_): assertionFailure("TODO") } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index d5e654dca..712a16970 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -22,42 +22,45 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid return } switch item { - case .account(let account, relationship: _): - await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) - case .status(let status): + case .account(let account, relationship: _): + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + case .status(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + case .hashtag(let tag): + await DataSourceFacade.coordinateToHashtagScene( + provider: self, + tag: tag + ) + case .notification(let notification): + let _status: MastodonStatus? = notification.status + if let status = _status { await DataSourceFacade.coordinateToStatusThreadScene( provider: self, target: .status, // remove reblog wrapper status: status ) - case .hashtag(let tag): - await DataSourceFacade.coordinateToHashtagScene( - provider: self, - tag: tag + } else if let accountWarning = notification.entity.accountWarning { + let url = Mastodon.API.disputesEndpoint(domain: authContext.mastodonAuthenticationBox.domain, strikeId: accountWarning.id) + _ = coordinator.present( + scene: .safari(url: url), + from: self, + transition: .safariPresent(animated: true, completion: nil) ) - case .notification(let notification): - let _status: MastodonStatus? = notification.status - if let status = _status { - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - } else if let accountWarning = notification.entity.accountWarning { - let url = Mastodon.API.disputesEndpoint(domain: authContext.mastodonAuthenticationBox.domain, strikeId: accountWarning.id) - _ = coordinator.present( - scene: .safari(url: url), - from: self, - transition: .safariPresent(animated: true, completion: nil) - ) - } else { - await DataSourceFacade.coordinateToProfileScene( - provider: self, - account: notification.entity.account - ) - } // end Task - } // end func + } else { + await DataSourceFacade.coordinateToProfileScene( + provider: self, + account: notification.entity.account + ) + } + case .notificationBanner(let policy): + //TODO: Coordinate to pending notification-screen + break + } } } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index fe886800e..b6e1134fb 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -14,6 +14,7 @@ enum DataSourceItem: Hashable { case status(record: MastodonStatus) case hashtag(tag: Mastodon.Entity.Tag) case notification(record: MastodonNotification) + case notificationBanner(policy: Mastodon.Entity.NotificationPolicy) case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) } diff --git a/Mastodon/Scene/Notification/Cell/NotificationFilteringBannerTableViewCell.swift b/Mastodon/Scene/Notification/Cell/NotificationFilteringBannerTableViewCell.swift new file mode 100644 index 000000000..409673e2e --- /dev/null +++ b/Mastodon/Scene/Notification/Cell/NotificationFilteringBannerTableViewCell.swift @@ -0,0 +1,22 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonSDK + +class NotificationFilteringBannerTableViewCell: UITableViewCell { + static let reuseIdentifier = "NotificationFilteringBannerTableViewCell" + + //TODO: Add separator + + func configure(with policy: Mastodon.Entity.NotificationPolicy) { + var configuration = defaultContentConfiguration() + + //TODO: Add localization + configuration.text = "Filtered notifications" + configuration.secondaryText = "\(policy.summary.pendingRequestsCount) people you may know" + configuration.image = UIImage(systemName: "archivebox") + + self.contentConfiguration = configuration + + } +} diff --git a/Mastodon/Scene/Notification/NotificationItem.swift b/Mastodon/Scene/Notification/NotificationItem.swift index d5727e813..72ca3d845 100644 --- a/Mastodon/Scene/Notification/NotificationItem.swift +++ b/Mastodon/Scene/Notification/NotificationItem.swift @@ -10,6 +10,7 @@ import Foundation import MastodonSDK enum NotificationItem: Hashable { + case filteredNotifications(policy: Mastodon.Entity.NotificationPolicy) case feed(record: MastodonFeed) case feedLoader(record: MastodonFeed) case bottomLoader diff --git a/Mastodon/Scene/Notification/NotificationSection.swift b/Mastodon/Scene/Notification/NotificationSection.swift index ac80faf97..cb630b904 100644 --- a/Mastodon/Scene/Notification/NotificationSection.swift +++ b/Mastodon/Scene/Notification/NotificationSection.swift @@ -39,6 +39,7 @@ extension NotificationSection { tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) tableView.register(AccountWarningNotificationCell.self, forCellReuseIdentifier: AccountWarningNotificationCell.reuseIdentifier) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + tableView.register(NotificationFilteringBannerTableViewCell.self, forCellReuseIdentifier: NotificationFilteringBannerTableViewCell.reuseIdentifier) return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { @@ -67,6 +68,12 @@ extension NotificationSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell cell.activityIndicatorView.startAnimating() return cell + + case .filteredNotifications(let policy): + let cell = tableView.dequeueReusableCell(withIdentifier: NotificationFilteringBannerTableViewCell.reuseIdentifier, for: indexPath) as! NotificationFilteringBannerTableViewCell + cell.configure(with: policy) + + return cell } } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index e4bcc8a12..04a8820a4 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -33,7 +33,9 @@ extension NotificationTimelineViewController: DataSourceProvider { } }() return item - default: + case .filteredNotifications(let policy): + return DataSourceItem.notificationBanner(policy: policy) + case .bottomLoader, .feedLoader(_): return nil } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index cd8a3158e..bdb00cb6d 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -13,16 +13,16 @@ import MastodonLocalization final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + weak var context: AppContext! + weak var coordinator: SceneCoordinator! let mediaPreviewTransitionController = MediaPreviewTransitionController() var disposeBag = Set() var observations = Set() - var viewModel: NotificationTimelineViewModel! - + let viewModel: NotificationTimelineViewModel + private(set) lazy var refreshControl: RefreshControl = { let refreshControl = RefreshControl() refreshControl.addTarget(self, action: #selector(NotificationTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) @@ -38,6 +38,16 @@ final class NotificationTimelineViewController: UIViewController, NeedsDependenc }() let cellFrameCache = NSCache() + + init(viewModel: NotificationTimelineViewModel, context: AppContext, coordinator: SceneCoordinator) { + self.viewModel = viewModel + self.context = context + self.coordinator = coordinator + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension NotificationTimelineViewController { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 9c5bf2949..6070f48d4 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -33,7 +33,7 @@ extension NotificationTimelineViewModel { dataController.$records .receive(on: DispatchQueue.main) .sink { [weak self] records in - guard let self = self else { return } + guard let self else { return } guard let diffableDataSource = self.diffableDataSource else { return } Task { @@ -44,6 +44,9 @@ extension NotificationTimelineViewModel { } var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) + if self.scope == .everything, let notificationPolicy = self.notificationPolicy, notificationPolicy.summary.pendingRequestsCount > 0 { + snapshot.appendItems([.filteredNotifications(policy: notificationPolicy)]) + } snapshot.appendItems(newItems.removingDuplicates(), toSection: .main) return snapshot }() diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index 0df34434a..6282c3773 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -20,6 +20,7 @@ final class NotificationTimelineViewModel { let context: AppContext let authContext: AuthContext let scope: Scope + let notificationPolicy: Mastodon.Entity.NotificationPolicy? let dataController: FeedDataController @Published var isLoadingLatest = false @Published var lastAutomaticFetchTimestamp: Date? @@ -46,12 +47,14 @@ final class NotificationTimelineViewModel { init( context: AppContext, authContext: AuthContext, - scope: Scope + scope: Scope, + notificationPolicy: Mastodon.Entity.NotificationPolicy? ) { self.context = context self.authContext = authContext self.scope = scope self.dataController = FeedDataController(context: context, authContext: authContext) + self.notificationPolicy = notificationPolicy switch scope { case .everything: diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 44c9d29d8..bcfeaf01e 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -146,17 +146,20 @@ extension NotificationViewController { pageSegmentedControl.selectedSegmentIndex = 0 } } - + private func createViewController(for scope: NotificationTimelineViewModel.Scope) -> UIViewController { - guard let authContext = viewModel?.authContext else { return UITableViewController() } - let viewController = NotificationTimelineViewController() - viewController.context = context - viewController.coordinator = coordinator - viewController.viewModel = NotificationTimelineViewModel( + guard let viewModel else { return UITableViewController() } + + let viewController = NotificationTimelineViewController( + viewModel: NotificationTimelineViewModel( + context: context, + authContext: viewModel.authContext, + scope: scope, notificationPolicy: viewModel.notificationPolicy + ), context: context, - authContext: authContext, - scope: scope + coordinator: coordinator ) + return viewController } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index ff1eab32b..4891b175b 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -70,7 +70,7 @@ extension SearchResultViewController { provider: self, tag: tag ) - case .notification: + case .notification, .notificationBanner(_): assertionFailure() } // end switch diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift index 4f487eb2c..68755e9b6 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift @@ -3,12 +3,12 @@ import Foundation extension Mastodon.Entity { - public struct NotificationPolicy: Codable { - let filterNotFollowing: Bool - let filterNotFollowers: Bool - let filterNewAccounts: Bool - let filterPrivateMentions: Bool - let summary: Summary + public struct NotificationPolicy: Codable, Hashable { + public let filterNotFollowing: Bool + public let filterNotFollowers: Bool + public let filterNewAccounts: Bool + public let filterPrivateMentions: Bool + public let summary: Summary enum CodingKeys: String, CodingKey { case filterNotFollowing = "filter_not_following" @@ -18,9 +18,9 @@ extension Mastodon.Entity { case summary } - public struct Summary: Codable { - let pendingRequestsCount: Int - let pendingNotificationsCount: Int + public struct Summary: Codable, Hashable { + public let pendingRequestsCount: Int + public let pendingNotificationsCount: Int enum CodingKeys: String, CodingKey { case pendingRequestsCount = "pending_requests_count"