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 */; };
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 = "<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; };
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>"; };
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>"; };
@ -1512,6 +1514,7 @@
DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */,
DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */,
D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */,
D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */,
);
path = Cell;
sourceTree = "<group>";
@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

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

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
enum NotificationItem: Hashable {
case filteredNotifications(policy: Mastodon.Entity.NotificationPolicy)
case feed(record: MastodonFeed)
case feedLoader(record: MastodonFeed)
case bottomLoader

View File

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

View File

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

View File

@ -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<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
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<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 {

View File

@ -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<NotificationSection, NotificationItem>()
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
}()

View File

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

View File

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

View File

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

View File

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