Files
GitHawk/Classes/Notifications/NotificationsViewController.swift

308 lines
10 KiB
Swift

//
// NotificationsViewController.swift
// Freetime
//
// Created by Ryan Nystrom on 6/8/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import IGListKit
import FlatCache
import Squawk
final class NotificationsViewController: BaseListViewController2<Int>,
BaseListViewController2DataSource,
ForegroundHandlerDelegate,
FlatCacheListener,
TabNavRootViewControllerType,
BaseListViewController2EmptyDataSource {
private let modelController: NotificationModelController
private let foreground = ForegroundHandler(threshold: 5 * 60)
private let inboxType: InboxType
private var notificationIDs = [String]()
private var notifications: [NotificationViewModel] {
return notificationIDs.compactMap { modelController.githubClient.cache.get(id: $0) }
}
init(modelController: NotificationModelController, inboxType: InboxType) {
self.modelController = modelController
self.inboxType = inboxType
super.init(emptyErrorMessage: NSLocalizedString("Cannot load your inbox.", comment: ""))
self.dataSource = self
self.emptyDataSource = self
self.foreground.delegate = self
switch inboxType {
case .all: title = NSLocalizedString("All", comment: "")
case .unread: title = NSLocalizedString("Inbox", comment: "")
case .repo(let repo): title = repo.name
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
makeBackBarItemEmpty()
resetRightBarItem()
switch inboxType {
case .unread:
let item = UIBarButtonItem(
image: UIImage(named: "bullets-hollow"),
style: .plain,
target: self,
action: #selector(NotificationsViewController.onMore(sender:))
)
item.accessibilityLabel = Constants.Strings.moreOptions
navigationItem.leftBarButtonItem = item
case .repo, .all: break
}
navigationController?.tabBarItem.badgeColor = Styles.Colors.Red.medium.color
}
override func fetch(page: Int?) {
let width = view.bounds.width
let showAll = inboxType.showAll
let repo: Repository?
switch inboxType {
case .repo(let r): repo = r
case .all, .unread: repo = nil
}
if let page = page {
modelController.fetchNotifications(repo: repo, all: showAll, page: page, width: width) { [weak self] result in
self?.handle(result: result, append: true, animated: false, page: page)
}
} else {
let first = 1
modelController.fetchNotifications(repo: repo, all: showAll, page: first, width: width) { [weak self] result in
self?.handle(result: result, append: false, animated: trueUnlessReduceMotionEnabled, page: first)
}
}
}
private func handle(result: Result<([NotificationViewModel], Int?)>, append: Bool, animated: Bool, page: Int) {
switch result {
case .success(let notifications, let next):
var ids = [String]()
notifications.forEach {
modelController.githubClient.cache.add(listener: self, value: $0)
ids.append($0.id)
}
rebuildAndUpdate(ids: ids, append: append, page: next, animated: animated)
case .error:
error(animated: trueUnlessReduceMotionEnabled)
Squawk.showNetworkError()
}
// set after updating so self.models has already been changed
updateUnreadState()
}
private func updateUnreadState() {
// don't update tab bar and badges when not showing only new notifications
// prevents archives updating badge and tab #s
switch inboxType {
case .repo: return
case .all, .unread: break
}
var unread = 0
for id in notificationIDs {
guard let model = modelController.githubClient.cache.get(id: id) as NotificationViewModel?,
!model.read
else { continue }
unread += 1
}
let hasUnread = unread > 0
navigationItem.rightBarButtonItem?.isEnabled = hasUnread
navigationController?.tabBarItem.badgeValue = hasUnread ? "\(unread)" : nil
BadgeNotifications.update(count: unread)
}
@objc func onMore(sender: UIBarButtonItem) {
let alert = UIAlertController.configured(preferredStyle: .actionSheet)
alert.add(action: UIAlertAction(
title: NSLocalizedString("View All", comment: ""),
style: .default,
handler: { [weak self] _ in
self?.onViewAll()
}))
let cache = modelController.githubClient.cache
var repoNames = Set<String>()
for id in notificationIDs {
guard let model = cache.get(id: id) as NotificationViewModel?,
!repoNames.contains(model.repo)
else { continue }
repoNames.insert(model.repo)
alert.add(action: UIAlertAction(title: model.repo, style: .default, handler: { [weak self] _ in
self?.pushRepoNotifications(owner: model.owner, repo: model.repo)
}))
}
alert.add(action: AlertAction.cancel())
alert.popoverPresentationController?.barButtonItem = sender
present(alert, animated: trueUnlessReduceMotionEnabled)
}
func pushRepoNotifications(owner: String, repo: String) {
let controller = NotificationsViewController(
modelController: modelController,
inboxType: .repo(Repository(owner: owner, name: repo))
)
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
}
func onViewAll() {
let controller = NotificationsViewController(
modelController: modelController,
inboxType: .all
)
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
}
func resetRightBarItem(updatingState updateState: Bool = true) {
let item = UIBarButtonItem(
image: UIImage(named: "check"),
style: .plain,
target: self,
action: #selector(onMarkAll)
)
item.accessibilityLabel = NSLocalizedString("Mark notifications read", comment: "")
navigationItem.rightBarButtonItem = item
if updateState {
updateUnreadState()
}
}
@objc private func onMarkAll() {
let message: String
switch inboxType {
case .all, .unread:
message = NSLocalizedString("Mark all notifications as read?", comment: "")
case .repo(let repo):
let messageFormat = NSLocalizedString("Mark %@ notifications as read?", comment: "")
message = String(format: messageFormat, repo.name)
}
let alert = UIAlertController.configured(
title: NSLocalizedString("Notifications", comment: ""),
message: message,
preferredStyle: .alert
)
alert.addActions([
UIAlertAction(
title: Constants.Strings.markRead,
style: .destructive,
handler: { [weak self] _ in
self?.markRead()
}),
AlertAction.cancel()
])
present(alert, animated: trueUnlessReduceMotionEnabled)
}
private func markRead() {
self.setRightBarItemSpinning()
let block: (Bool) -> Void = { success in
let generator = UINotificationFeedbackGenerator()
if success {
generator.notificationOccurred(.success)
// clear all badges
BadgeNotifications.update(count: 0)
// change the spinner to the mark all item
// don't update state here; it is managed by `fetch`
self.resetRightBarItem(updatingState: false)
} else {
generator.notificationOccurred(.error)
}
self.fetch(page: nil)
// "mark all" is an engaging action, system prompt on it
RatingController.prompt(.system)
}
switch inboxType {
case .all, .unread: modelController.markAllNotifications(completion: block)
case .repo(let repo): modelController.markRepoNotifications(repo: repo, completion: block)
}
}
private func rebuildAndUpdate(
ids: [String],
append: Bool,
page: Int?,
animated: Bool
) {
if append {
notificationIDs += ids
} else {
notificationIDs = ids
}
update(page: page, animated: animated)
}
// MARK: BaseListViewController2DataSource
func models(adapter: ListSwiftAdapter) -> [ListSwiftPair] {
return notificationIDs.compactMap { id in
guard let model = modelController.githubClient.cache.get(id: id) as NotificationViewModel?
else { return nil }
return ListSwiftPair.pair(model) { [modelController] in
NotificationSectionController(modelController: modelController)
}
}
}
// MARK: BaseListViewController2EmptyDataSource
func emptyModel(for adapter: ListSwiftAdapter) -> ListSwiftPair {
let layoutInsets = view.safeAreaInsets
return ListSwiftPair.pair("empty-notification-value", {
return NoNewNotificationSectionController(layoutInsets: layoutInsets)
})
}
// MARK: ForegroundHandlerDelegate
func didForeground(handler: ForegroundHandler) {
feed.refreshHead()
}
// MARK: FlatCacheListener
func flatCacheDidUpdate(cache: FlatCache, update: FlatCache.Update) {
self.update(animated: trueUnlessReduceMotionEnabled)
updateUnreadState()
}
// MARK: TabNavRootViewControllerType
func didSingleTapTab() {
feed.collectionView.scrollToTop(animated: true)
}
func didDoubleTapTab() {
didSingleTapTab()
}
}