Add and show filter-banner in "everything"-notifications (if there are any) (IOS-241)

This commit is contained in:
Nathan Mattes 2024-06-25 12:09:24 +02:00
parent f297cbabc5
commit 4843348034
16 changed files with 137 additions and 84 deletions

View File

@ -158,6 +158,7 @@
D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */; }; D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */; };
D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A892A4468DC00C0FB73 /* AboutViewController.swift */; }; D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A892A4468DC00C0FB73 /* AboutViewController.swift */; };
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.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 */; }; D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */; };
D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; }; D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; };
D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; }; 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 = "<group>"; }; D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewController.swift; sourceTree = "<group>"; };
D8318A892A4468DC00C0FB73 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; }; D8318A892A4468DC00C0FB73 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = "<group>"; tabWidth = 4; }; D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = "<group>"; tabWidth = 4; };
D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFilteringBannerTableViewCell.swift; sourceTree = "<group>"; };
D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusPill.swift; sourceTree = "<group>"; }; D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusPill.swift; sourceTree = "<group>"; };
D84C099D2B0F9E33009E685E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; }; D84C099D2B0F9E33009E685E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
D84C099F2B0F9E41009E685E /* Setup.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Setup.md; sourceTree = "<group>"; }; D84C099F2B0F9E41009E685E /* Setup.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Setup.md; sourceTree = "<group>"; };
@ -1512,6 +1514,7 @@
DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */, DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */,
DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */, DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */,
D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */, D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */,
D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */,
); );
path = Cell; path = Cell;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3780,6 +3783,7 @@
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */,
D83B54F82C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift in Sources */,
DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */,
D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */, D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */,
D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */, D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */,

View File

@ -17,32 +17,30 @@ extension DataSourceFacade {
item: DataSourceItem item: DataSourceItem
) async { ) async {
switch item { switch item {
case .account(account: let account, relationship: _): case .account(account: let account, relationship: _):
let now = Date() let now = Date()
let userID = provider.authContext.mastodonAuthenticationBox.userID let userID = provider.authContext.mastodonAuthenticationBox.userID
let searchEntry = Persistence.SearchHistory.Item( let searchEntry = Persistence.SearchHistory.Item(
updatedAt: now, updatedAt: now,
userID: userID, userID: userID,
account: account, account: account,
hashtag: nil hashtag: nil
) )
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
case .hashtag(let tag): case .hashtag(let tag):
let now = Date() let now = Date()
let userID = provider.authContext.mastodonAuthenticationBox.userID let userID = provider.authContext.mastodonAuthenticationBox.userID
let searchEntry = Persistence.SearchHistory.Item( let searchEntry = Persistence.SearchHistory.Item(
updatedAt: now, updatedAt: now,
userID: userID, userID: userID,
account: nil, account: nil,
hashtag: tag hashtag: tag
) )
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
case .status: case .status, .notification, .notificationBanner(_):
break
case .notification:
break break
} }

View File

@ -514,10 +514,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
) )
case .account(let account, _): case .account(let account, _):
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
case .notification: case .notification, .hashtag(_), .notificationBanner(_):
assertionFailure("TODO") print("TODO")
case .hashtag(_):
assertionFailure("TODO")
} }
} // end Task } // end Task
} }

View File

@ -618,9 +618,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
provider: self, provider: self,
account: account account: account
) )
case .notification: case .notification, .hashtag(_), .notificationBanner(_):
assertionFailure("TODO")
case .hashtag(_):
assertionFailure("TODO") assertionFailure("TODO")
} }
} }

View File

@ -22,42 +22,45 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
return return
} }
switch item { switch item {
case .account(let account, relationship: _): case .account(let account, relationship: _):
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
case .status(let status): 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( await DataSourceFacade.coordinateToStatusThreadScene(
provider: self, provider: self,
target: .status, // remove reblog wrapper target: .status, // remove reblog wrapper
status: status status: status
) )
case .hashtag(let tag): } else if let accountWarning = notification.entity.accountWarning {
await DataSourceFacade.coordinateToHashtagScene( let url = Mastodon.API.disputesEndpoint(domain: authContext.mastodonAuthenticationBox.domain, strikeId: accountWarning.id)
provider: self, _ = coordinator.present(
tag: tag 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 { } else {
await DataSourceFacade.coordinateToProfileScene( await DataSourceFacade.coordinateToProfileScene(
provider: self, provider: self,
account: notification.entity.account account: notification.entity.account
) )
} // end Task }
} // end func case .notificationBanner(let policy):
//TODO: Coordinate to pending notification-screen
break
}
} }
} }
} }

View File

@ -14,6 +14,7 @@ enum DataSourceItem: Hashable {
case status(record: MastodonStatus) case status(record: MastodonStatus)
case hashtag(tag: Mastodon.Entity.Tag) case hashtag(tag: Mastodon.Entity.Tag)
case notification(record: MastodonNotification) case notification(record: MastodonNotification)
case notificationBanner(policy: Mastodon.Entity.NotificationPolicy)
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
} }

View File

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

View File

@ -10,6 +10,7 @@ import Foundation
import MastodonSDK import MastodonSDK
enum NotificationItem: Hashable { enum NotificationItem: Hashable {
case filteredNotifications(policy: Mastodon.Entity.NotificationPolicy)
case feed(record: MastodonFeed) case feed(record: MastodonFeed)
case feedLoader(record: MastodonFeed) case feedLoader(record: MastodonFeed)
case bottomLoader case bottomLoader

View File

@ -39,6 +39,7 @@ extension NotificationSection {
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
tableView.register(AccountWarningNotificationCell.self, forCellReuseIdentifier: AccountWarningNotificationCell.reuseIdentifier) tableView.register(AccountWarningNotificationCell.self, forCellReuseIdentifier: AccountWarningNotificationCell.reuseIdentifier)
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) 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 return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
switch item { switch item {
@ -67,6 +68,12 @@ extension NotificationSection {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
cell.activityIndicatorView.startAnimating() cell.activityIndicatorView.startAnimating()
return cell return cell
case .filteredNotifications(let policy):
let cell = tableView.dequeueReusableCell(withIdentifier: NotificationFilteringBannerTableViewCell.reuseIdentifier, for: indexPath) as! NotificationFilteringBannerTableViewCell
cell.configure(with: policy)
return cell
} }
} }
} }

View File

@ -33,7 +33,9 @@ extension NotificationTimelineViewController: DataSourceProvider {
} }
}() }()
return item return item
default: case .filteredNotifications(let policy):
return DataSourceItem.notificationBanner(policy: policy)
case .bottomLoader, .feedLoader(_):
return nil return nil
} }
} }

View File

@ -13,16 +13,16 @@ import MastodonLocalization
final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var context: AppContext!
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator!
let mediaPreviewTransitionController = MediaPreviewTransitionController() let mediaPreviewTransitionController = MediaPreviewTransitionController()
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>() var observations = Set<NSKeyValueObservation>()
var viewModel: NotificationTimelineViewModel! let viewModel: NotificationTimelineViewModel
private(set) lazy var refreshControl: RefreshControl = { private(set) lazy var refreshControl: RefreshControl = {
let refreshControl = RefreshControl() let refreshControl = RefreshControl()
refreshControl.addTarget(self, action: #selector(NotificationTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) refreshControl.addTarget(self, action: #selector(NotificationTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
@ -38,6 +38,16 @@ final class NotificationTimelineViewController: UIViewController, NeedsDependenc
}() }()
let cellFrameCache = NSCache<NSNumber, NSValue>() let cellFrameCache = NSCache<NSNumber, NSValue>()
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 { extension NotificationTimelineViewController {

View File

@ -33,7 +33,7 @@ extension NotificationTimelineViewModel {
dataController.$records dataController.$records
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] records in .sink { [weak self] records in
guard let self = self else { return } guard let self else { return }
guard let diffableDataSource = self.diffableDataSource else { return } guard let diffableDataSource = self.diffableDataSource else { return }
Task { Task {
@ -44,6 +44,9 @@ extension NotificationTimelineViewModel {
} }
var snapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>() var snapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
snapshot.appendSections([.main]) 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) snapshot.appendItems(newItems.removingDuplicates(), toSection: .main)
return snapshot return snapshot
}() }()

View File

@ -20,6 +20,7 @@ final class NotificationTimelineViewModel {
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
let scope: Scope let scope: Scope
let notificationPolicy: Mastodon.Entity.NotificationPolicy?
let dataController: FeedDataController let dataController: FeedDataController
@Published var isLoadingLatest = false @Published var isLoadingLatest = false
@Published var lastAutomaticFetchTimestamp: Date? @Published var lastAutomaticFetchTimestamp: Date?
@ -46,12 +47,14 @@ final class NotificationTimelineViewModel {
init( init(
context: AppContext, context: AppContext,
authContext: AuthContext, authContext: AuthContext,
scope: Scope scope: Scope,
notificationPolicy: Mastodon.Entity.NotificationPolicy?
) { ) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.scope = scope self.scope = scope
self.dataController = FeedDataController(context: context, authContext: authContext) self.dataController = FeedDataController(context: context, authContext: authContext)
self.notificationPolicy = notificationPolicy
switch scope { switch scope {
case .everything: case .everything:

View File

@ -146,17 +146,20 @@ extension NotificationViewController {
pageSegmentedControl.selectedSegmentIndex = 0 pageSegmentedControl.selectedSegmentIndex = 0
} }
} }
private func createViewController(for scope: NotificationTimelineViewModel.Scope) -> UIViewController { private func createViewController(for scope: NotificationTimelineViewModel.Scope) -> UIViewController {
guard let authContext = viewModel?.authContext else { return UITableViewController() } guard let viewModel else { return UITableViewController() }
let viewController = NotificationTimelineViewController()
viewController.context = context let viewController = NotificationTimelineViewController(
viewController.coordinator = coordinator viewModel: NotificationTimelineViewModel(
viewController.viewModel = NotificationTimelineViewModel( context: context,
authContext: viewModel.authContext,
scope: scope, notificationPolicy: viewModel.notificationPolicy
),
context: context, context: context,
authContext: authContext, coordinator: coordinator
scope: scope
) )
return viewController return viewController
} }
} }

View File

@ -70,7 +70,7 @@ extension SearchResultViewController {
provider: self, provider: self,
tag: tag tag: tag
) )
case .notification: case .notification, .notificationBanner(_):
assertionFailure() assertionFailure()
} // end switch } // end switch

View File

@ -3,12 +3,12 @@
import Foundation import Foundation
extension Mastodon.Entity { extension Mastodon.Entity {
public struct NotificationPolicy: Codable { public struct NotificationPolicy: Codable, Hashable {
let filterNotFollowing: Bool public let filterNotFollowing: Bool
let filterNotFollowers: Bool public let filterNotFollowers: Bool
let filterNewAccounts: Bool public let filterNewAccounts: Bool
let filterPrivateMentions: Bool public let filterPrivateMentions: Bool
let summary: Summary public let summary: Summary
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case filterNotFollowing = "filter_not_following" case filterNotFollowing = "filter_not_following"
@ -18,9 +18,9 @@ extension Mastodon.Entity {
case summary case summary
} }
public struct Summary: Codable { public struct Summary: Codable, Hashable {
let pendingRequestsCount: Int public let pendingRequestsCount: Int
let pendingNotificationsCount: Int public let pendingNotificationsCount: Int
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case pendingRequestsCount = "pending_requests_count" case pendingRequestsCount = "pending_requests_count"