Tusker/Tusker/Screens/Notifications/NotificationsTableViewController.swift
Shadowfacts 2bdcb9b7f8
Replace global shared MastodonController instance with (mostly)
dependency injection

The places still using the .shared property are cases where there is no
view controller from which to (easily) get the appropriate instance,
such as user activity and X-Callback-URL handling. These uses will need
to be revisited once there are multiple MastodonControllers.

See #16
2020-01-05 15:25:07 -05:00

255 lines
11 KiB
Swift

//
// NotificationsTableViewController.swift
// Tusker
//
// Created by Shadowfacts on 9/2/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class NotificationsTableViewController: EnhancedTableViewController {
private let statusCell = "statusCell"
private let actionGroupCell = "actionGroupCell"
private let followGroupCell = "followGroupCell"
private let followRequestCell = "followRequestCell"
let mastodonController: MastodonController
let excludedTypes: [Pachyderm.Notification.Kind]
let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
var groups: [NotificationGroup] = [] {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
var newer: RequestRange?
var older: RequestRange?
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes))
self.mastodonController = mastodonController
super.init(style: .plain)
self.refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshNotifications(_:)), for: .valueChanged)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
tableView.prefetchDataSource = self
let request = Client.getNotifications(excludeTypes: excludedTypes)
mastodonController.run(request) { result in
guard case let .success(notifications, pagination) = result else { fatalError() }
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
self.groups.append(contentsOf: groups)
MastodonCache.addAll(notifications: notifications)
MastodonCache.addAll(statuses: notifications.compactMap { $0.status })
MastodonCache.addAll(accounts: notifications.map { $0.account })
self.newer = pagination?.newer
self.older = pagination?.older
}
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return groups.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let group = groups[indexPath.row]
switch group.kind {
case .mention:
guard let notification = MastodonCache.notification(for: group.notificationIDs.first!),
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else {
fatalError()
}
cell.updateUI(statusID: notification.status!.id, state: group.statusState!)
cell.delegate = self
return cell
case .favourite, .reblog:
guard let cell = tableView.dequeueReusableCell(withIdentifier: actionGroupCell, for: indexPath) as? ActionNotificationGroupTableViewCell else { fatalError() }
cell.updateUI(group: group)
cell.delegate = self
return cell
case .follow:
guard let cell = tableView.dequeueReusableCell(withIdentifier: followGroupCell, for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() }
cell.updateUI(group: group)
cell.delegate = self
return cell
case .followRequest:
guard let notification = MastodonCache.notification(for: group.notificationIDs.first!),
let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() }
cell.updateUI(notification: notification)
cell.delegate = self
return cell
}
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == groups.count - 1 {
guard let older = older else { return }
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
mastodonController.run(request) { result in
guard case let .success(newNotifications, pagination) = result else { fatalError() }
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.groups.append(contentsOf: groups)
MastodonCache.addAll(notifications: newNotifications)
MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status })
MastodonCache.addAll(accounts: newNotifications.map { $0.account })
self.older = pagination?.older
}
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let dismissAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Dismiss", comment: "dismiss notification swipe action title")) { (action, view, completion) in
self.dismissNotificationsInGroup(at: indexPath) {
completion(true)
}
}
dismissAction.image = UIImage(systemName: "clear.fill")
let cellConfiguration = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
let config: UISwipeActionsConfiguration
if let cellConfiguration = cellConfiguration {
config = UISwipeActionsConfiguration(actions: cellConfiguration.actions + [dismissAction])
config.performsFirstActionWithFullSwipe = cellConfiguration.performsFirstActionWithFullSwipe
} else {
config = UISwipeActionsConfiguration(actions: [dismissAction])
config.performsFirstActionWithFullSwipe = false
}
return config
}
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
return [
UIAction(title: "Dismiss Notification", image: UIImage(systemName: "clear.fill"), identifier: .init("dismissnotification"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
self.dismissNotificationsInGroup(at: indexPath)
})
]
}
func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
let group = DispatchGroup()
groups[indexPath.row].notificationIDs
.map(Pachyderm.Notification.dismiss(id:))
.forEach { (request) in
group.enter()
mastodonController.run(request) { (response) in
group.leave()
}
}
group.notify(queue: .main) {
self.groups.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
completion?()
}
}
@objc func refreshNotifications(_ sender: Any) {
guard let newer = newer else { return }
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
mastodonController.run(request) { result in
guard case let .success(newNotifications, pagination) = result else { fatalError() }
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.groups.insert(contentsOf: groups, at: 0)
MastodonCache.addAll(notifications: newNotifications)
MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status })
MastodonCache.addAll(accounts: newNotifications.map { $0.account })
self.newer = pagination?.newer
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
// maintain the current position in the list (don't scroll to top)
self.tableView.scrollToRow(at: IndexPath(row: newNotifications.count, section: 0), at: .top, animated: false)
}
}
}
}
extension NotificationsTableViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights
tableView.beginUpdates()
tableView.endUpdates()
}
}
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
for notificationID in groups[indexPath.row].notificationIDs {
guard let notification = MastodonCache.notification(for: notificationID) else { continue }
ImageCache.avatars.get(notification.account.avatar, completion: nil)
}
}
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
for notificationID in groups[indexPath.row].notificationIDs {
guard let notification = MastodonCache.notification(for: notificationID) else { continue }
ImageCache.avatars.cancel(notification.account.avatar)
}
}
}
}