forked from shadowfacts/Tusker
Shadowfacts
2bdcb9b7f8
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
255 lines
11 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|