Convert notifications to use DiffableTimelineLikeTableViewController

This commit is contained in:
Shadowfacts 2021-08-15 19:25:29 -04:00
parent c0097ba752
commit 9026f487ec
4 changed files with 143 additions and 99 deletions

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class NotificationGroup { public class NotificationGroup: Identifiable, Hashable {
public let notifications: [Notification] public let notifications: [Notification]
public let id: String public let id: String
public let kind: Notification.Kind public let kind: Notification.Kind
@ -26,6 +26,14 @@ public class NotificationGroup {
} }
} }
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
return lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] { public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
var groups = [[Notification]]() var groups = [[Notification]]()
for notification in notifications { for notification in notifications {
@ -50,5 +58,3 @@ public class NotificationGroup {
} }
} }
extension NotificationGroup: Identifiable {}

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class NotificationsTableViewController: TimelineLikeTableViewController<NotificationGroup> { class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationGroup> {
private let statusCell = "statusCell" private let statusCell = "statusCell"
private let actionGroupCell = "actionGroupCell" private let actionGroupCell = "actionGroupCell"
@ -54,88 +54,9 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell) tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
} }
override func loadInitialItems(completion: @escaping ([NotificationGroup]) -> Void) { // MARK: - DiffableTimelineLikeTableViewController
let request = Client.getNotifications(excludeTypes: excludedTypes)
mastodonController.run(request) { (response) in
guard case let .success(notifications, pagination) = response else {
completion([])
return
}
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
self.newer = pagination?.newer
self.older = pagination?.older
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
completion(groups)
}
}
}
override func loadOlder(completion: @escaping ([NotificationGroup]) -> Void) {
guard let older = older else {
completion([])
return
}
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
mastodonController.run(request) { (response) in
guard case let .success(newNotifications, pagination) = response else { fatalError() }
self.older = pagination?.older
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
completion(groups)
}
}
}
override func loadNewer(completion: @escaping ([NotificationGroup]) -> Void) {
guard let newer = newer else {
completion([])
return
}
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
mastodonController.run(request) { (response) in
guard case let .success(newNotifications, pagination) = response else { fatalError() }
if let newer = pagination?.newer {
self.newer = newer
}
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
completion(groups)
}
}
}
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
let group = DispatchGroup()
item(for: indexPath).notifications
.map { Pachyderm.Notification.dismiss(id: $0.id) }
.forEach { (request) in
group.enter()
mastodonController.run(request) { (_) in
group.leave()
}
}
group.notify(queue: .main) {
self.sections[indexPath.section].remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
completion?()
}
}
// MARK: - UITableViewDataSource
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let group = item(for: indexPath)
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ group: NotificationGroup) -> UITableViewCell? {
switch group.kind { switch group.kind {
case .mention: case .mention:
guard let notification = group.notifications.first, guard let notification = group.notifications.first,
@ -179,6 +100,112 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
} }
} }
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
let request = Client.getNotifications(excludeTypes: excludedTypes)
mastodonController.run(request) { (response) in
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(notifications, pagination):
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
self.newer = pagination?.newer
self.older = pagination?.older
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
var snapshot = Snapshot()
snapshot.appendSections([.notifications])
snapshot.appendItems(groups, toSection: .notifications)
completion(.success(snapshot))
}
}
}
}
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let older = older else {
completion(.failure(.noOlder))
return
}
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
mastodonController.run(request) { (response) in
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(newNotifications, pagination):
if let older = pagination?.older {
self.older = older
}
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
var snapshot = currentSnapshot()
snapshot.appendItems(groups, toSection: .notifications)
completion(.success(snapshot))
}
}
}
}
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let newer = newer else {
completion(.failure(.noNewer))
return
}
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
mastodonController.run(request) { (response) in
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(newNotifications, pagination):
guard !newNotifications.isEmpty else {
completion(.failure(.allCaughtUp))
return
}
if let newer = pagination?.newer {
self.newer = newer
}
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
var snapshot = currentSnapshot()
if let first = snapshot.itemIdentifiers(inSection: .notifications).first {
snapshot.insertItems(groups, beforeItem: first)
} else {
snapshot.appendItems(groups, toSection: .notifications)
}
completion(.success(snapshot))
}
}
}
}
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
let group = DispatchGroup()
item.notifications
.map { Pachyderm.Notification.dismiss(id: $0.id) }
.forEach { (request) in
group.enter()
mastodonController.run(request) { (_) in
group.leave()
}
}
group.notify(queue: .main) {
var snapshot = self.dataSource.snapshot()
snapshot.deleteItems([item])
self.dataSource.apply(snapshot, completion: completion)
}
}
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
@ -211,6 +238,12 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
} }
} }
extension NotificationsTableViewController {
enum Section: CaseIterable, Hashable {
case notifications
}
}
extension NotificationsTableViewController: TuskerNavigationDelegate { extension NotificationsTableViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController } var apiController: MastodonController { mastodonController }
} }
@ -224,7 +257,8 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
extension NotificationsTableViewController: UITableViewDataSourcePrefetching { extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
for notification in item(for: indexPath).notifications { guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
for notification in group.notifications {
ImageCache.avatars.fetchIfNotCached(notification.account.avatar) ImageCache.avatars.fetchIfNotCached(notification.account.avatar)
} }
} }
@ -232,7 +266,8 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
for notification in item(for: indexPath).notifications { guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
for notification in group.notifications {
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar) ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
} }
} }

View File

@ -221,6 +221,11 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
completion(.failure(.client(error))) completion(.failure(.client(error)))
case let .success(statuses, pagination): case let .success(statuses, pagination):
guard !statuses.isEmpty else {
completion(.failure(.allCaughtUp))
return
}
// if there are no new statuses, pagination is nil // if there are no new statuses, pagination is nil
// if we were to then overwrite self.newer, future refresh would fail // if we were to then overwrite self.newer, future refresh would fail
if let newer = pagination?.newer { if let newer = pagination?.newer {
@ -236,18 +241,6 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
snapshot.appendItems(newIdentifiers, toSection: .statuses) snapshot.appendItems(newIdentifiers, toSection: .statuses)
} }
completion(.success(snapshot)) completion(.success(snapshot))
if statuses.isEmpty {
DispatchQueue.main.async {
var config = ToastConfiguration(title: "You're all caught up")
config.edge = .top
config.dismissAutomaticallyAfter = 2
config.action = { (toast) in
toast.dismissToast(animated: true)
}
self.showToast(configuration: config, animated: true)
}
}
} }
} }
} }

View File

@ -252,6 +252,15 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
} }
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
case .failure(.allCaughtUp):
var config = ToastConfiguration(title: "You're all caught up")
config.edge = .top
config.dismissAutomaticallyAfter = 2
config.action = { (toast) in
toast.dismissToast(animated: true)
}
self.showToast(configuration: config, animated: true)
default: default:
break break
} }
@ -301,6 +310,7 @@ extension DiffableTimelineLikeTableViewController {
case noClient case noClient
case noOlder case noOlder
case noNewer case noNewer
case allCaughtUp
case client(Client.Error) case client(Client.Error)
} }
} }