2019-06-29 20:35:12 +02:00
|
|
|
|
//
|
|
|
|
|
// NavigationModelController.swift
|
|
|
|
|
// NetNewsWire-iOS
|
|
|
|
|
//
|
|
|
|
|
// Created by Maurice Parker on 4/21/19.
|
|
|
|
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
2019-09-07 21:00:31 +02:00
|
|
|
|
import UIKit
|
|
|
|
|
import SwiftUI
|
2019-06-29 20:35:12 +02:00
|
|
|
|
import Account
|
|
|
|
|
import Articles
|
|
|
|
|
import RSCore
|
|
|
|
|
import RSTree
|
|
|
|
|
|
2019-08-31 22:53:47 +02:00
|
|
|
|
enum SearchScope: Int {
|
|
|
|
|
case timeline = 0
|
|
|
|
|
case global = 1
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 19:43:07 +02:00
|
|
|
|
class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
2019-07-06 19:25:45 +02:00
|
|
|
|
|
|
|
|
|
var undoableCommands = [UndoableCommand]()
|
|
|
|
|
var undoManager: UndoManager? {
|
|
|
|
|
return rootSplitViewController.undoManager
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 02:30:21 +02:00
|
|
|
|
private var activityManager = ActivityManager()
|
|
|
|
|
|
2019-09-04 23:24:16 +02:00
|
|
|
|
private var rootSplitViewController: RootSplitViewController!
|
2019-07-06 00:45:39 +02:00
|
|
|
|
private var masterNavigationController: UINavigationController!
|
|
|
|
|
private var masterFeedViewController: MasterFeedViewController!
|
|
|
|
|
private var masterTimelineViewController: MasterTimelineViewController?
|
|
|
|
|
|
2019-09-10 14:22:32 +02:00
|
|
|
|
private var subSplitViewController: UISplitViewController? {
|
|
|
|
|
return rootSplitViewController.children.last?.children.first as? UISplitViewController
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 18:49:53 +02:00
|
|
|
|
private var detailViewController: DetailViewController? {
|
2019-08-02 23:46:55 +02:00
|
|
|
|
if let detail = masterNavigationController.viewControllers.last as? DetailViewController {
|
|
|
|
|
return detail
|
|
|
|
|
}
|
2019-09-10 14:49:24 +02:00
|
|
|
|
if let subSplit = subSplitViewController {
|
2019-08-02 11:22:47 +02:00
|
|
|
|
if let navController = subSplit.viewControllers.last as? UINavigationController {
|
|
|
|
|
return navController.topViewController as? DetailViewController
|
|
|
|
|
}
|
2019-08-02 02:59:56 +02:00
|
|
|
|
} else {
|
2019-08-02 11:22:47 +02:00
|
|
|
|
if let navController = rootSplitViewController.viewControllers.last?.children.first as? UINavigationController {
|
|
|
|
|
return navController.topViewController as? DetailViewController
|
|
|
|
|
}
|
2019-07-06 18:49:53 +02:00
|
|
|
|
}
|
2019-08-02 11:22:47 +02:00
|
|
|
|
return nil
|
2019-07-06 18:49:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 00:45:39 +02:00
|
|
|
|
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
2019-08-21 22:27:53 +02:00
|
|
|
|
private var fetchSerialNumber = 0
|
|
|
|
|
private let fetchRequestQueue = FetchRequestQueue()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
|
|
|
|
|
|
|
|
|
private var animatingChanges = false
|
|
|
|
|
private var expandedNodes = [Node]()
|
|
|
|
|
private var shadowTable = [[Node]]()
|
2019-08-31 18:50:34 +02:00
|
|
|
|
private var lastSearchString = ""
|
2019-08-31 22:53:47 +02:00
|
|
|
|
private var lastSearchScope: SearchScope? = nil
|
2019-08-31 19:12:50 +02:00
|
|
|
|
private var isSearching: Bool = false
|
2019-08-31 22:53:47 +02:00
|
|
|
|
private var searchArticleIds: Set<String>? = nil
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-08-19 00:34:53 +02:00
|
|
|
|
private(set) var sortDirection = AppDefaults.timelineSortDirection {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
didSet {
|
|
|
|
|
if sortDirection != oldValue {
|
|
|
|
|
sortDirectionDidChange()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private let treeControllerDelegate = FeedTreeControllerDelegate()
|
2019-09-01 21:56:27 +02:00
|
|
|
|
private lazy var treeController: TreeController = {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return TreeController(delegate: treeControllerDelegate)
|
|
|
|
|
}()
|
|
|
|
|
|
2019-09-01 02:30:21 +02:00
|
|
|
|
var stateRestorationActivity: NSUserActivity? {
|
|
|
|
|
return activityManager.stateRestorationActivity
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-02 17:25:47 +02:00
|
|
|
|
var isRootSplitCollapsed: Bool {
|
|
|
|
|
return rootSplitViewController.isCollapsed
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-27 21:49:07 +02:00
|
|
|
|
var isThreePanelMode: Bool {
|
2019-09-10 14:22:32 +02:00
|
|
|
|
return subSplitViewController != nil
|
2019-09-09 23:21:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
var rootNode: Node {
|
|
|
|
|
return treeController.rootNode
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-29 01:06:27 +02:00
|
|
|
|
var allSections: [Int] {
|
|
|
|
|
var sections = [Int]()
|
|
|
|
|
for (index, _) in shadowTable.enumerated() {
|
|
|
|
|
sections.append(index)
|
|
|
|
|
}
|
|
|
|
|
return sections
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 04:06:29 +02:00
|
|
|
|
private(set) var currentFeedIndexPath: IndexPath?
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
var timelineName: String? {
|
|
|
|
|
return (timelineFetcher as? DisplayNameProvider)?.nameForDisplay
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var timelineFetcher: ArticleFetcher? {
|
|
|
|
|
didSet {
|
2019-08-31 19:12:50 +02:00
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
if timelineFetcher is Feed {
|
|
|
|
|
showFeedNames = false
|
|
|
|
|
} else {
|
|
|
|
|
showFeedNames = true
|
|
|
|
|
}
|
2019-08-31 19:12:50 +02:00
|
|
|
|
|
|
|
|
|
if isSearching {
|
|
|
|
|
fetchAndReplaceArticlesAsync {
|
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
fetchAndReplaceArticlesSync()
|
|
|
|
|
masterTimelineViewController?.reinitializeArticles()
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 18:32:19 +02:00
|
|
|
|
private(set) var showFeedNames = false
|
|
|
|
|
private(set) var showAvatars = false
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-09-05 04:06:29 +02:00
|
|
|
|
var isPrevFeedAvailable: Bool {
|
|
|
|
|
guard let indexPath = currentFeedIndexPath else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return indexPath.section > 0 || indexPath.row > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isNextFeedAvailable: Bool {
|
|
|
|
|
guard let indexPath = currentFeedIndexPath else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let nextIndexPath: IndexPath = {
|
|
|
|
|
if indexPath.row + 1 >= shadowTable[indexPath.section].count {
|
|
|
|
|
return IndexPath(row: 0, section: indexPath.section + 1)
|
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return nextIndexPath.section < shadowTable.count && nextIndexPath.row < shadowTable[nextIndexPath.section].count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prevFeedIndexPath: IndexPath? {
|
|
|
|
|
guard isPrevFeedAvailable, let indexPath = currentFeedIndexPath else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let prevIndexPath: IndexPath = {
|
|
|
|
|
if indexPath.row - 1 < 0 {
|
|
|
|
|
return IndexPath(row: shadowTable[indexPath.section - 1].count - 1, section: indexPath.section - 1)
|
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return prevIndexPath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nextFeedIndexPath: IndexPath? {
|
|
|
|
|
guard isNextFeedAvailable, let indexPath = currentFeedIndexPath else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let nextIndexPath: IndexPath = {
|
|
|
|
|
if indexPath.row + 1 >= shadowTable[indexPath.section].count {
|
|
|
|
|
return IndexPath(row: 0, section: indexPath.section + 1)
|
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return nextIndexPath
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
var isPrevArticleAvailable: Bool {
|
|
|
|
|
guard let indexPath = currentArticleIndexPath else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return indexPath.row > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isNextArticleAvailable: Bool {
|
|
|
|
|
guard let indexPath = currentArticleIndexPath else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return indexPath.row + 1 < articles.count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prevArticleIndexPath: IndexPath? {
|
2019-09-05 04:06:29 +02:00
|
|
|
|
guard isPrevArticleAvailable, let indexPath = currentArticleIndexPath else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nextArticleIndexPath: IndexPath? {
|
2019-09-05 04:06:29 +02:00
|
|
|
|
guard isNextArticleAvailable, let indexPath = currentArticleIndexPath else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var firstUnreadArticleIndexPath: IndexPath? {
|
|
|
|
|
for (row, article) in articles.enumerated() {
|
|
|
|
|
if !article.status.read {
|
|
|
|
|
return IndexPath(row: row, section: 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var currentArticle: Article? {
|
2019-08-30 21:42:33 +02:00
|
|
|
|
if let indexPath = currentArticleIndexPath, indexPath.row < articles.count {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return articles[indexPath.row]
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 19:52:06 +02:00
|
|
|
|
private(set) var currentArticleIndexPath: IndexPath?
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-09-03 23:07:02 +02:00
|
|
|
|
private(set) var articles = ArticleArray()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
var isTimelineUnreadAvailable: Bool {
|
|
|
|
|
if let unreadProvider = timelineFetcher as? UnreadCountProvider {
|
|
|
|
|
return unreadProvider.unreadCount > 0
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isAnyUnreadAvailable: Bool {
|
|
|
|
|
return appDelegate.unreadCount > 0
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 20:10:08 +02:00
|
|
|
|
var unreadCount: Int = 0 {
|
|
|
|
|
didSet {
|
|
|
|
|
if unreadCount != oldValue {
|
|
|
|
|
postUnreadCountDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-24 21:57:51 +02:00
|
|
|
|
|
2019-07-19 22:59:46 +02:00
|
|
|
|
override init() {
|
2019-07-19 19:29:17 +02:00
|
|
|
|
super.init()
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
for section in treeController.rootNode.childNodes {
|
|
|
|
|
expandedNodes.append(section)
|
|
|
|
|
shadowTable.append([Node]())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rebuildShadowTable()
|
|
|
|
|
|
2019-08-21 20:10:08 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
|
2019-09-08 16:43:51 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddAccount(_:)), name: .UserDidAddAccount, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDidDeleteAccount(_:)), name: .UserDidDeleteAccount, object: nil)
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
2019-08-31 21:53:03 +02:00
|
|
|
|
|
|
|
|
|
// Force lazy initialization of the web view provider so that it can warm up the queue of prepared web views
|
|
|
|
|
let _ = DetailViewControllerWebViewProvider.shared
|
2019-07-19 22:59:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-09 23:59:24 +02:00
|
|
|
|
func start(for size: CGSize) -> UIViewController {
|
2019-09-04 23:24:16 +02:00
|
|
|
|
rootSplitViewController = RootSplitViewController()
|
|
|
|
|
rootSplitViewController.coordinator = self
|
2019-09-10 00:40:18 +02:00
|
|
|
|
rootSplitViewController.preferredDisplayMode = .allVisible
|
2019-09-04 23:24:16 +02:00
|
|
|
|
rootSplitViewController.viewControllers = [ThemedNavigationController.template()]
|
2019-07-06 00:45:39 +02:00
|
|
|
|
rootSplitViewController.delegate = self
|
|
|
|
|
|
|
|
|
|
masterNavigationController = (rootSplitViewController.viewControllers.first as! UINavigationController)
|
2019-09-01 22:31:11 +02:00
|
|
|
|
masterNavigationController.delegate = self
|
2019-08-23 19:27:45 +02:00
|
|
|
|
|
2019-07-19 22:59:46 +02:00
|
|
|
|
masterFeedViewController = UIStoryboard.main.instantiateController(ofType: MasterFeedViewController.self)
|
2019-07-06 00:45:39 +02:00
|
|
|
|
masterFeedViewController.coordinator = self
|
2019-07-19 22:59:46 +02:00
|
|
|
|
masterNavigationController.pushViewController(masterFeedViewController, animated: false)
|
2019-07-06 00:45:39 +02:00
|
|
|
|
|
2019-09-10 00:40:18 +02:00
|
|
|
|
let noSelectionController = fullyWrappedSystemMesssageController(showButton: true)
|
|
|
|
|
rootSplitViewController.showDetailViewController(noSelectionController, sender: self)
|
2019-07-26 23:26:22 +02:00
|
|
|
|
|
2019-09-09 23:59:24 +02:00
|
|
|
|
configureThreePanelMode(for: size)
|
|
|
|
|
|
2019-07-19 22:59:46 +02:00
|
|
|
|
return rootSplitViewController
|
2019-07-06 00:45:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-25 02:31:29 +02:00
|
|
|
|
func handle(_ activity: NSUserActivity) {
|
2019-09-06 14:29:36 +02:00
|
|
|
|
selectFeed(nil)
|
|
|
|
|
|
2019-08-25 02:31:29 +02:00
|
|
|
|
guard let activityType = ActivityType(rawValue: activity.activityType) else { return }
|
|
|
|
|
switch activityType {
|
2019-08-26 00:04:15 +02:00
|
|
|
|
case .selectToday:
|
|
|
|
|
handleSelectToday()
|
|
|
|
|
case .selectAllUnread:
|
|
|
|
|
handleSelectAllUnread()
|
|
|
|
|
case .selectStarred:
|
|
|
|
|
handleSelectStarred()
|
2019-08-27 21:20:34 +02:00
|
|
|
|
case .selectFolder:
|
2019-08-28 00:43:15 +02:00
|
|
|
|
handleSelectFolder(activity)
|
2019-08-27 21:20:34 +02:00
|
|
|
|
case .selectFeed:
|
2019-08-28 00:43:15 +02:00
|
|
|
|
handleSelectFeed(activity)
|
2019-09-03 22:52:59 +02:00
|
|
|
|
case .nextUnread:
|
|
|
|
|
selectFirstUnreadInAllUnread()
|
2019-08-25 02:31:29 +02:00
|
|
|
|
case .readArticle:
|
|
|
|
|
handleReadArticle(activity)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-09 23:59:24 +02:00
|
|
|
|
func configureThreePanelMode(for size: CGSize) {
|
|
|
|
|
guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad && !rootSplitViewController.isCollapsed else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if size.width > size.height {
|
|
|
|
|
if !isThreePanelMode {
|
|
|
|
|
transitionToThreePanelMode()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if isThreePanelMode {
|
|
|
|
|
transitionFromThreePanelMode()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-02 00:41:46 +02:00
|
|
|
|
func selectFirstUnreadInAllUnread() {
|
|
|
|
|
selectFeed(IndexPath(row: 1, section: 0))
|
|
|
|
|
selectFirstUnreadArticleInTimeline()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func showSearch() {
|
|
|
|
|
selectFeed(nil)
|
2019-09-07 16:01:29 +02:00
|
|
|
|
installTimelineControllerIfNecessary(animated: false)
|
2019-09-06 17:29:00 +02:00
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
|
|
|
|
self.masterTimelineViewController!.showSearchAll()
|
|
|
|
|
}
|
2019-09-02 00:41:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
// MARK: Notifications
|
|
|
|
|
|
2019-08-21 20:10:08 +02:00
|
|
|
|
@objc func statusesDidChange(_ note: Notification) {
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
@objc func containerChildrenDidChange(_ note: Notification) {
|
2019-08-29 22:02:45 +02:00
|
|
|
|
if timelineFetcherContainsAnyPseudoFeed() || timelineFetcherContainsAnyFolder() {
|
2019-09-01 17:29:30 +02:00
|
|
|
|
fetchAndReplaceArticlesAsync() {}
|
2019-08-29 22:02:45 +02:00
|
|
|
|
}
|
2019-09-08 16:43:51 +02:00
|
|
|
|
rebuildBackingStores()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func batchUpdateDidPerform(_ notification: Notification) {
|
|
|
|
|
rebuildBackingStores()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func displayNameDidChange(_ note: Notification) {
|
|
|
|
|
rebuildBackingStores()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func accountStateDidChange(_ note: Notification) {
|
2019-08-21 22:27:53 +02:00
|
|
|
|
if timelineFetcherContainsAnyPseudoFeed() {
|
2019-08-29 21:46:51 +02:00
|
|
|
|
fetchAndReplaceArticlesSync()
|
2019-08-21 22:27:53 +02:00
|
|
|
|
}
|
2019-09-08 16:58:27 +02:00
|
|
|
|
|
|
|
|
|
guard let account = note.userInfo?[Account.UserInfoKey.account] as? Account else {
|
|
|
|
|
assertionFailure()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we are deactivating an account, clean up the expandedNodes table
|
|
|
|
|
if !account.isActive, let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
|
|
|
|
if let nodeIndex = self.expandedNodes.firstIndex(of: node) {
|
|
|
|
|
self.expandedNodes.remove(at: nodeIndex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rebuildBackingStores() {
|
|
|
|
|
// If we are activating an account, then automatically expand it
|
|
|
|
|
if account.isActive, let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
|
|
|
|
self.expandedNodes.append(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-08 16:43:51 +02:00
|
|
|
|
@objc func userDidAddAccount(_ note: Notification) {
|
2019-08-21 22:27:53 +02:00
|
|
|
|
if timelineFetcherContainsAnyPseudoFeed() {
|
2019-08-29 21:46:51 +02:00
|
|
|
|
fetchAndReplaceArticlesSync()
|
2019-08-21 22:27:53 +02:00
|
|
|
|
}
|
2019-09-08 16:43:51 +02:00
|
|
|
|
|
|
|
|
|
rebuildBackingStores() {
|
2019-09-08 16:58:27 +02:00
|
|
|
|
// Automatically expand any new accounts
|
2019-09-08 16:43:51 +02:00
|
|
|
|
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
|
|
|
|
|
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
|
|
|
|
self.expandedNodes.append(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func userDidDeleteAccount(_ note: Notification) {
|
|
|
|
|
if timelineFetcherContainsAnyPseudoFeed() {
|
|
|
|
|
fetchAndReplaceArticlesSync()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rebuildBackingStores() {
|
2019-09-08 16:58:27 +02:00
|
|
|
|
// Clean up the expandedNodes table for any deleted accounts
|
2019-09-08 16:43:51 +02:00
|
|
|
|
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
|
|
|
|
|
let node = self.treeController.rootNode.childNodeRepresentingObject(account),
|
|
|
|
|
let nodeIndex = self.expandedNodes.firstIndex(of: node) {
|
|
|
|
|
self.expandedNodes.remove(at: nodeIndex)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func userDefaultsDidChange(_ note: Notification) {
|
|
|
|
|
self.sortDirection = AppDefaults.timelineSortDirection
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func accountDidDownloadArticles(_ note: Notification) {
|
|
|
|
|
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let shouldFetchAndMergeArticles = timelineFetcherContainsAnyFeed(feeds) || timelineFetcherContainsAnyPseudoFeed()
|
|
|
|
|
if shouldFetchAndMergeArticles {
|
|
|
|
|
queueFetchAndMergeArticles()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: API
|
|
|
|
|
|
|
|
|
|
func rowsInSection(_ section: Int) -> Int {
|
|
|
|
|
return shadowTable[section].count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isExpanded(_ node: Node) -> Bool {
|
|
|
|
|
return expandedNodes.contains(node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nodeFor(_ indexPath: IndexPath) -> Node? {
|
2019-08-23 20:23:13 +02:00
|
|
|
|
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return shadowTable[indexPath.section][indexPath.row]
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-29 01:06:27 +02:00
|
|
|
|
func nodesFor(section: Int) -> [Node] {
|
|
|
|
|
return shadowTable[section]
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-03 19:07:18 +02:00
|
|
|
|
func cappedIndexPath(_ indexPath: IndexPath) -> IndexPath {
|
|
|
|
|
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
|
|
|
|
|
return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
|
|
|
|
|
}
|
|
|
|
|
return indexPath
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
func indexPathFor(_ node: Node) -> IndexPath? {
|
|
|
|
|
for i in 0..<shadowTable.count {
|
|
|
|
|
if let row = shadowTable[i].firstIndex(of: node) {
|
|
|
|
|
return IndexPath(row: row, section: i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-08-21 20:10:08 +02:00
|
|
|
|
|
2019-08-26 00:08:39 +02:00
|
|
|
|
func indexPathFor(_ object: AnyObject) -> IndexPath? {
|
2019-08-26 00:04:15 +02:00
|
|
|
|
guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return indexPathFor(node)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 20:10:08 +02:00
|
|
|
|
func unreadCountFor(_ node: Node) -> Int {
|
|
|
|
|
// The coordinator supplies the unread count for the currently selected feed node
|
2019-09-05 04:06:29 +02:00
|
|
|
|
if let indexPath = currentFeedIndexPath, let selectedNode = nodeFor(indexPath), selectedNode == node {
|
2019-08-21 20:10:08 +02:00
|
|
|
|
return unreadCount
|
|
|
|
|
}
|
|
|
|
|
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
|
|
|
|
|
return unreadCountProvider.unreadCount
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:08:57 +02:00
|
|
|
|
func expandSection(_ section: Int) {
|
2019-09-05 23:38:33 +02:00
|
|
|
|
guard let expandNode = treeController.rootNode.childAtIndex(section), !expandedNodes.contains(expandNode) else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-09-05 23:38:33 +02:00
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
expandedNodes.append(expandNode)
|
|
|
|
|
|
|
|
|
|
animatingChanges = true
|
|
|
|
|
|
|
|
|
|
var i = 0
|
|
|
|
|
|
|
|
|
|
func addNode(_ node: Node) {
|
|
|
|
|
shadowTable[section].insert(node, at: i)
|
|
|
|
|
i = i + 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for child in expandNode.childNodes {
|
|
|
|
|
addNode(child)
|
|
|
|
|
if expandedNodes.contains(child) {
|
|
|
|
|
for gChild in child.childNodes {
|
|
|
|
|
addNode(gChild)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animatingChanges = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:38:33 +02:00
|
|
|
|
func expandAllSectionsAndFolders() {
|
|
|
|
|
for (sectionIndex, sectionNode) in treeController.rootNode.childNodes.enumerated() {
|
|
|
|
|
|
|
|
|
|
expandSection(sectionIndex)
|
|
|
|
|
|
|
|
|
|
for topLevelNode in sectionNode.childNodes {
|
|
|
|
|
if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) {
|
|
|
|
|
expandFolder(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:08:57 +02:00
|
|
|
|
func expandFolder(_ indexPath: IndexPath) {
|
2019-09-09 19:08:47 +02:00
|
|
|
|
guard let expandNode = nodeFor(indexPath), !expandedNodes.contains(expandNode) && expandNode.representedObject is Folder else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
expandedNodes.append(expandNode)
|
|
|
|
|
|
|
|
|
|
animatingChanges = true
|
|
|
|
|
|
|
|
|
|
for i in 0..<expandNode.childNodes.count {
|
|
|
|
|
if let child = expandNode.childAtIndex(i) {
|
|
|
|
|
let nextIndex = indexPath.row + i + 1
|
|
|
|
|
shadowTable[indexPath.section].insert(child, at: nextIndex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animatingChanges = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:08:57 +02:00
|
|
|
|
func collapseSection(_ section: Int) {
|
2019-09-05 23:38:33 +02:00
|
|
|
|
guard let collapseNode = treeController.rootNode.childAtIndex(section), expandedNodes.contains(collapseNode) else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:38:33 +02:00
|
|
|
|
animatingChanges = true
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
if let removeNode = expandedNodes.firstIndex(of: collapseNode) {
|
|
|
|
|
expandedNodes.remove(at: removeNode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shadowTable[section] = [Node]()
|
|
|
|
|
|
|
|
|
|
animatingChanges = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:38:33 +02:00
|
|
|
|
func collapseAllFolders() {
|
|
|
|
|
for sectionNode in treeController.rootNode.childNodes {
|
|
|
|
|
for topLevelNode in sectionNode.childNodes {
|
|
|
|
|
if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) {
|
|
|
|
|
collapseFolder(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:08:57 +02:00
|
|
|
|
func collapseFolder(_ indexPath: IndexPath) {
|
2019-09-09 19:08:47 +02:00
|
|
|
|
guard let collapseNode = nodeFor(indexPath), expandedNodes.contains(collapseNode) && collapseNode.representedObject is Folder else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = true
|
|
|
|
|
|
|
|
|
|
if let removeNode = expandedNodes.firstIndex(of: collapseNode) {
|
|
|
|
|
expandedNodes.remove(at: removeNode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for child in collapseNode.childNodes {
|
|
|
|
|
if let index = shadowTable[indexPath.section].firstIndex(of: child) {
|
|
|
|
|
shadowTable[indexPath.section].remove(at: index)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animatingChanges = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-03 20:59:22 +02:00
|
|
|
|
func masterFeedIndexPathForCurrentTimeline() -> IndexPath? {
|
|
|
|
|
guard let node = treeController.rootNode.descendantNode(where: { return $0.representedObject === timelineFetcher as AnyObject }) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return indexPathFor(node)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-30 21:17:05 +02:00
|
|
|
|
func indexForArticleID(_ articleID: String?) -> Int? {
|
|
|
|
|
guard let articleID = articleID else { return nil }
|
|
|
|
|
updateArticleRowMapIfNeeded()
|
|
|
|
|
return articleRowMap[articleID]
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
|
|
|
|
|
var indexes = IndexSet()
|
|
|
|
|
|
|
|
|
|
articleIDs.forEach { (articleID) in
|
2019-08-30 21:17:05 +02:00
|
|
|
|
guard let oneIndex = indexForArticleID(articleID) else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if oneIndex != NSNotFound {
|
|
|
|
|
indexes.insert(oneIndex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return indexes
|
|
|
|
|
}
|
2019-08-30 21:17:05 +02:00
|
|
|
|
|
2019-09-06 14:29:36 +02:00
|
|
|
|
func selectFeed(_ indexPath: IndexPath?, automated: Bool = true) {
|
|
|
|
|
selectArticle(nil)
|
2019-09-05 04:06:29 +02:00
|
|
|
|
currentFeedIndexPath = indexPath
|
2019-09-02 00:41:46 +02:00
|
|
|
|
|
2019-09-06 20:45:45 +02:00
|
|
|
|
masterFeedViewController.updateFeedSelection()
|
|
|
|
|
|
2019-09-02 00:41:46 +02:00
|
|
|
|
if let ip = indexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher {
|
|
|
|
|
timelineFetcher = fetcher
|
|
|
|
|
updateSelectingActivity(with: node)
|
2019-09-07 16:01:29 +02:00
|
|
|
|
installTimelineControllerIfNecessary(animated: !automated)
|
2019-09-02 00:41:46 +02:00
|
|
|
|
} else {
|
|
|
|
|
timelineFetcher = nil
|
2019-09-06 14:29:36 +02:00
|
|
|
|
|
|
|
|
|
if rootSplitViewController.isCollapsed && navControllerForTimeline().viewControllers.last is MasterTimelineViewController {
|
|
|
|
|
navControllerForTimeline().popViewController(animated: !automated)
|
|
|
|
|
}
|
2019-09-02 00:41:46 +02:00
|
|
|
|
}
|
2019-07-27 21:36:01 +02:00
|
|
|
|
|
2019-07-06 18:32:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 04:06:29 +02:00
|
|
|
|
func selectPrevFeed() {
|
|
|
|
|
if let indexPath = prevFeedIndexPath {
|
|
|
|
|
selectFeed(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectNextFeed() {
|
|
|
|
|
if let indexPath = nextFeedIndexPath {
|
|
|
|
|
selectFeed(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-05 20:14:14 +02:00
|
|
|
|
|
|
|
|
|
func selectTodayFeed() {
|
|
|
|
|
masterFeedViewController?.ensureSectionIsExpanded(0) {
|
|
|
|
|
self.selectFeed(IndexPath(row: 0, section: 0))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectAllUnreadFeed() {
|
|
|
|
|
masterFeedViewController?.ensureSectionIsExpanded(0) {
|
|
|
|
|
self.selectFeed(IndexPath(row: 1, section: 0))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectStarredFeed() {
|
|
|
|
|
masterFeedViewController?.ensureSectionIsExpanded(0) {
|
|
|
|
|
self.selectFeed(IndexPath(row: 2, section: 0))
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-05 04:06:29 +02:00
|
|
|
|
|
2019-09-02 19:05:11 +02:00
|
|
|
|
func selectArticle(_ indexPath: IndexPath?, automated: Bool = true) {
|
2019-08-01 13:53:34 +02:00
|
|
|
|
currentArticleIndexPath = indexPath
|
2019-09-01 02:30:21 +02:00
|
|
|
|
activityManager.reading(currentArticle)
|
2019-08-24 21:57:51 +02:00
|
|
|
|
|
2019-08-01 13:53:34 +02:00
|
|
|
|
if indexPath == nil {
|
2019-09-06 14:29:36 +02:00
|
|
|
|
if rootSplitViewController.isCollapsed {
|
|
|
|
|
if masterNavigationController.children.last is DetailViewController {
|
2019-09-06 17:29:00 +02:00
|
|
|
|
masterNavigationController.popViewController(animated: !automated)
|
2019-09-06 14:29:36 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2019-08-01 13:53:34 +02:00
|
|
|
|
let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
|
2019-09-06 14:29:36 +02:00
|
|
|
|
installDetailController(systemMessageViewController, automated: automated)
|
2019-08-01 13:53:34 +02:00
|
|
|
|
}
|
2019-09-06 17:29:00 +02:00
|
|
|
|
masterTimelineViewController?.updateArticleSelection(animate: !automated)
|
2019-08-01 13:53:34 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if detailViewController == nil {
|
2019-07-19 22:59:46 +02:00
|
|
|
|
let detailViewController = UIStoryboard.main.instantiateController(ofType: DetailViewController.self)
|
2019-07-06 18:32:19 +02:00
|
|
|
|
detailViewController.coordinator = self
|
2019-09-06 14:29:36 +02:00
|
|
|
|
installDetailController(detailViewController, automated: automated)
|
2019-07-26 23:26:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Automatically hide the overlay
|
|
|
|
|
if rootSplitViewController.displayMode == .primaryOverlay {
|
|
|
|
|
UIView.animate(withDuration: 0.3) {
|
|
|
|
|
self.rootSplitViewController.preferredDisplayMode = .primaryHidden
|
|
|
|
|
}
|
|
|
|
|
rootSplitViewController.preferredDisplayMode = .automatic
|
2019-07-06 18:32:19 +02:00
|
|
|
|
}
|
2019-09-01 19:52:06 +02:00
|
|
|
|
|
2019-09-02 19:05:11 +02:00
|
|
|
|
if automated {
|
2019-09-06 14:29:36 +02:00
|
|
|
|
masterTimelineViewController?.updateArticleSelection(animate: false)
|
2019-09-02 19:05:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 19:52:06 +02:00
|
|
|
|
detailViewController?.updateArticleSelection()
|
|
|
|
|
|
2019-09-06 20:45:45 +02:00
|
|
|
|
if let article = currentArticle {
|
|
|
|
|
markArticles(Set([article]), statusKey: .read, flag: true)
|
|
|
|
|
}
|
2019-07-06 18:32:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 21:49:56 +02:00
|
|
|
|
func beginSearching() {
|
|
|
|
|
isSearching = true
|
|
|
|
|
searchArticleIds = Set(articles.map { $0.articleID })
|
2019-09-04 17:42:28 +02:00
|
|
|
|
timelineFetcher = nil
|
2019-09-01 21:49:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func endSearching() {
|
|
|
|
|
isSearching = false
|
|
|
|
|
lastSearchString = ""
|
|
|
|
|
lastSearchScope = nil
|
|
|
|
|
searchArticleIds = nil
|
2019-08-31 18:50:34 +02:00
|
|
|
|
|
2019-09-05 04:06:29 +02:00
|
|
|
|
if let ip = currentFeedIndexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher {
|
2019-09-01 21:49:56 +02:00
|
|
|
|
timelineFetcher = fetcher
|
2019-09-03 19:25:27 +02:00
|
|
|
|
} else {
|
|
|
|
|
timelineFetcher = nil
|
2019-08-31 22:53:47 +02:00
|
|
|
|
}
|
2019-09-01 21:49:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func searchArticles(_ searchString: String, _ searchScope: SearchScope) {
|
|
|
|
|
|
|
|
|
|
guard isSearching else { return }
|
|
|
|
|
|
2019-08-31 19:12:50 +02:00
|
|
|
|
if searchString.count < 3 {
|
|
|
|
|
timelineFetcher = nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-31 22:53:47 +02:00
|
|
|
|
if searchString != lastSearchString || searchScope != lastSearchScope {
|
|
|
|
|
|
|
|
|
|
switch searchScope {
|
|
|
|
|
case .global:
|
|
|
|
|
timelineFetcher = SmartFeed(delegate: SearchFeedDelegate(searchString: searchString))
|
|
|
|
|
case .timeline:
|
|
|
|
|
timelineFetcher = SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: searchArticleIds!))
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-31 18:50:34 +02:00
|
|
|
|
lastSearchString = searchString
|
2019-08-31 22:53:47 +02:00
|
|
|
|
lastSearchScope = searchScope
|
2019-08-31 18:50:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 18:32:19 +02:00
|
|
|
|
func selectPrevArticle() {
|
|
|
|
|
if let indexPath = prevArticleIndexPath {
|
|
|
|
|
selectArticle(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectNextArticle() {
|
|
|
|
|
if let indexPath = nextArticleIndexPath {
|
|
|
|
|
selectArticle(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-04 00:07:43 +02:00
|
|
|
|
func selectFirstUnread() {
|
2019-09-03 22:52:59 +02:00
|
|
|
|
if selectFirstUnreadArticleInTimeline() {
|
|
|
|
|
activityManager.selectingNextUnread()
|
|
|
|
|
}
|
2019-08-04 00:07:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-06 01:02:40 +02:00
|
|
|
|
func selectPrevUnread() {
|
|
|
|
|
|
|
|
|
|
// This should never happen, but I don't want to risk throwing us
|
|
|
|
|
// into an infinate loop searching for an unread that isn't there.
|
|
|
|
|
if appDelegate.unreadCount < 1 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if selectPrevUnreadArticleInTimeline() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectPrevUnreadFeedFetcher()
|
|
|
|
|
selectPrevUnreadArticleInTimeline()
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
func selectNextUnread() {
|
|
|
|
|
|
|
|
|
|
// This should never happen, but I don't want to risk throwing us
|
|
|
|
|
// into an infinate loop searching for an unread that isn't there.
|
|
|
|
|
if appDelegate.unreadCount < 1 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if selectNextUnreadArticleInTimeline() {
|
2019-09-03 22:52:59 +02:00
|
|
|
|
activityManager.selectingNextUnread()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectNextUnreadFeedFetcher()
|
2019-09-03 22:52:59 +02:00
|
|
|
|
if selectNextUnreadArticleInTimeline() {
|
|
|
|
|
activityManager.selectingNextUnread()
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-06 04:14:19 +02:00
|
|
|
|
func scrollOrGoToNextUnread() {
|
|
|
|
|
if detailViewController?.canScrollDown() ?? false {
|
|
|
|
|
detailViewController?.scrollPageDown()
|
|
|
|
|
} else {
|
|
|
|
|
selectNextUnread()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-20 00:26:09 +02:00
|
|
|
|
func markAllAsRead(_ articles: [Article]) {
|
|
|
|
|
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
runCommand(markReadCommand)
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 19:25:45 +02:00
|
|
|
|
func markAllAsRead() {
|
|
|
|
|
let accounts = AccountManager.shared.activeAccounts
|
|
|
|
|
var articles = Set<Article>()
|
|
|
|
|
accounts.forEach { account in
|
2019-07-12 20:01:34 +02:00
|
|
|
|
articles.formUnion(account.fetchArticles(.unread))
|
2019-07-06 19:25:45 +02:00
|
|
|
|
}
|
2019-08-20 00:26:09 +02:00
|
|
|
|
markAllAsRead(Array(articles))
|
2019-07-06 19:25:45 +02:00
|
|
|
|
}
|
2019-08-20 00:26:09 +02:00
|
|
|
|
|
2019-07-06 19:31:07 +02:00
|
|
|
|
func markAllAsReadInTimeline() {
|
2019-08-20 00:26:09 +02:00
|
|
|
|
markAllAsRead(articles)
|
2019-07-06 19:31:07 +02:00
|
|
|
|
masterNavigationController.popViewController(animated: true)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 21:50:05 +02:00
|
|
|
|
func markAsReadOlderArticlesInTimeline() {
|
|
|
|
|
if let indexPath = currentArticleIndexPath {
|
|
|
|
|
markAsReadOlderArticlesInTimeline(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-19 23:03:07 +02:00
|
|
|
|
func markAsReadOlderArticlesInTimeline(_ indexPath: IndexPath) {
|
|
|
|
|
let article = articles[indexPath.row]
|
|
|
|
|
let articlesToMark = articles.filter { $0.logicalDatePublished < article.logicalDatePublished }
|
|
|
|
|
if articlesToMark.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-08-20 12:46:44 +02:00
|
|
|
|
markAllAsRead(articlesToMark)
|
2019-08-19 23:03:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 21:50:05 +02:00
|
|
|
|
func markAsReadForCurrentArticle() {
|
|
|
|
|
if let article = currentArticle {
|
|
|
|
|
markArticles(Set([article]), statusKey: .read, flag: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func markAsUnreadForCurrentArticle() {
|
|
|
|
|
if let article = currentArticle {
|
|
|
|
|
markArticles(Set([article]), statusKey: .read, flag: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 18:49:53 +02:00
|
|
|
|
func toggleReadForCurrentArticle() {
|
|
|
|
|
if let article = currentArticle {
|
|
|
|
|
markArticles(Set([article]), statusKey: .read, flag: !article.status.read)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-19 23:03:07 +02:00
|
|
|
|
func toggleRead(for indexPath: IndexPath) {
|
|
|
|
|
let article = articles[indexPath.row]
|
|
|
|
|
guard let undoManager = undoManager,
|
|
|
|
|
let markReadCommand = MarkStatusCommand(initialArticles: [article], markingRead: !article.status.read, undoManager: undoManager) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
runCommand(markReadCommand)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 22:43:01 +02:00
|
|
|
|
func toggleStarredForCurrentArticle() {
|
2019-07-06 18:49:53 +02:00
|
|
|
|
if let article = currentArticle {
|
|
|
|
|
markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-19 23:03:07 +02:00
|
|
|
|
func toggleStar(for indexPath: IndexPath) {
|
|
|
|
|
let article = articles[indexPath.row]
|
|
|
|
|
guard let undoManager = undoManager,
|
|
|
|
|
let markReadCommand = MarkStatusCommand(initialArticles: [article], markingStarred: !article.status.starred, undoManager: undoManager) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
runCommand(markReadCommand)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 03:23:14 +02:00
|
|
|
|
func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) {
|
|
|
|
|
masterFeedViewController.discloseFeed(feed) {
|
|
|
|
|
completion?()
|
|
|
|
|
}
|
2019-08-19 22:45:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 19:25:45 +02:00
|
|
|
|
func showSettings() {
|
2019-09-07 21:00:31 +02:00
|
|
|
|
// let settingsNavViewController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController
|
|
|
|
|
// settingsNavViewController.modalPresentationStyle = .formSheet
|
|
|
|
|
// let settingsViewController = settingsNavViewController.topViewController as! SettingsViewController
|
|
|
|
|
// settingsViewController.presentingParentController = rootSplitViewController
|
|
|
|
|
// rootSplitViewController.present(settingsNavViewController, animated: true)
|
|
|
|
|
|
2019-09-07 23:43:44 +02:00
|
|
|
|
rootSplitViewController.present(style: .formSheet) {
|
|
|
|
|
SettingsView(viewModel: SettingsView.ViewModel())
|
|
|
|
|
}
|
2019-07-06 19:25:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 22:07:35 +02:00
|
|
|
|
func showAdd(_ type: AddControllerType) {
|
2019-09-06 17:57:37 +02:00
|
|
|
|
selectFeed(nil)
|
|
|
|
|
|
2019-09-05 22:07:35 +02:00
|
|
|
|
let addViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController
|
|
|
|
|
let containerController = addViewController.topViewController as! AddContainerViewController
|
|
|
|
|
containerController.initialControllerType = type
|
2019-07-06 19:25:45 +02:00
|
|
|
|
addViewController.modalPresentationStyle = .formSheet
|
|
|
|
|
addViewController.preferredContentSize = AddContainerViewController.preferredContentSizeForFormSheetDisplay
|
|
|
|
|
masterFeedViewController.present(addViewController, animated: true)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-04 23:24:16 +02:00
|
|
|
|
func homePageURLForFeed(_ indexPath: IndexPath) -> URL? {
|
|
|
|
|
guard let node = nodeFor(indexPath),
|
|
|
|
|
let feed = node.representedObject as? Feed,
|
|
|
|
|
let homePageURL = feed.homePageURL,
|
|
|
|
|
let url = URL(string: homePageURL) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func showBrowserForFeed(_ indexPath: IndexPath) {
|
|
|
|
|
if let url = homePageURLForFeed(indexPath) {
|
|
|
|
|
UIApplication.shared.open(url, options: [:])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func showBrowserForCurrentFeed() {
|
2019-09-05 04:06:29 +02:00
|
|
|
|
if let ip = currentFeedIndexPath, let url = homePageURLForFeed(ip) {
|
2019-09-04 23:24:16 +02:00
|
|
|
|
UIApplication.shared.open(url, options: [:])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func showBrowserForArticle(_ indexPath: IndexPath) {
|
2019-08-20 00:38:30 +02:00
|
|
|
|
guard let preferredLink = articles[indexPath.row].preferredLink, let url = URL(string: preferredLink) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
UIApplication.shared.open(url, options: [:])
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 18:49:53 +02:00
|
|
|
|
func showBrowserForCurrentArticle() {
|
|
|
|
|
guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
UIApplication.shared.open(url, options: [:])
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 04:06:29 +02:00
|
|
|
|
func navigateToFeeds() {
|
|
|
|
|
masterFeedViewController?.focus()
|
|
|
|
|
selectArticle(nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func navigateToTimeline() {
|
|
|
|
|
if currentArticleIndexPath == nil {
|
|
|
|
|
selectArticle(IndexPath(row: 0, section: 0))
|
|
|
|
|
}
|
2019-09-05 17:46:24 +02:00
|
|
|
|
masterTimelineViewController?.focus()
|
2019-09-05 04:06:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func navigateToDetail() {
|
|
|
|
|
detailViewController?.focus()
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: UISplitViewControllerDelegate
|
|
|
|
|
|
2019-09-01 19:43:07 +02:00
|
|
|
|
extension SceneCoordinator: UISplitViewControllerDelegate {
|
2019-08-01 23:14:34 +02:00
|
|
|
|
|
|
|
|
|
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
|
2019-08-03 17:55:46 +02:00
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Check to see if the system is currently configured for three panel mode
|
2019-08-02 23:46:55 +02:00
|
|
|
|
if let subSplit = secondaryViewController.children.first as? UISplitViewController {
|
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Take the timeline controller out of the subsplit and throw it on the master navigation stack
|
2019-08-03 18:09:12 +02:00
|
|
|
|
if let masterTimelineNav = subSplit.viewControllers.first as? UINavigationController, let masterTimeline = masterTimelineNav.topViewController {
|
2019-08-02 23:46:55 +02:00
|
|
|
|
masterNavigationController.pushViewController(masterTimeline, animated: false)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Take the detail view (ignoring system message controllers) and put it on the master navigation stack
|
2019-08-03 17:55:46 +02:00
|
|
|
|
if let detailNav = subSplit.viewControllers.last as? UINavigationController, let detail = detailNav.topViewController as? DetailViewController {
|
2019-08-02 23:46:55 +02:00
|
|
|
|
masterNavigationController.pushViewController(detail, animated: false)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-03 17:55:46 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// If the timeline controller has been initialized and only the feeds controller is on the stack, we add the timeline controller
|
2019-08-03 17:55:46 +02:00
|
|
|
|
if let timeline = masterTimelineViewController, masterNavigationController.viewControllers.count == 1 {
|
|
|
|
|
masterNavigationController.pushViewController(timeline, animated: false)
|
|
|
|
|
}
|
2019-08-02 23:46:55 +02:00
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Take the detail view (ignoring system message controllers) and put it on the master navigation stack
|
2019-08-03 17:55:46 +02:00
|
|
|
|
if let detailNav = secondaryViewController.children.first as? UINavigationController, let detail = detailNav.topViewController as? DetailViewController {
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// I have no idea why, I have to wire up the left bar button item for this, but not when I am transitioning from three panel mode
|
2019-08-03 18:44:05 +02:00
|
|
|
|
detail.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
|
|
|
|
|
detail.navigationItem.leftItemsSupplementBackButton = true
|
2019-08-03 17:55:46 +02:00
|
|
|
|
masterNavigationController.pushViewController(detail, animated: false)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-02 23:46:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-03 17:55:46 +02:00
|
|
|
|
return true
|
|
|
|
|
|
2019-08-01 23:14:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-02 23:46:55 +02:00
|
|
|
|
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
|
2019-08-03 20:53:30 +02:00
|
|
|
|
|
|
|
|
|
// If we are in three panel mode, return back the new shim controller that contains a new sub split controller
|
2019-08-02 23:46:55 +02:00
|
|
|
|
if isThreePanelMode {
|
|
|
|
|
return transitionToThreePanelMode()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let detail = masterNavigationController.viewControllers.last as? DetailViewController {
|
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// If we have a detail controller on the stack, remove it, wrap it in a shim, and return it.
|
2019-08-02 23:46:55 +02:00
|
|
|
|
masterNavigationController.viewControllers.removeLast()
|
|
|
|
|
let detailNav = addNavControllerIfNecessary(detail, showButton: true)
|
|
|
|
|
let shimController = UIViewController()
|
|
|
|
|
shimController.addChildAndPinView(detailNav)
|
|
|
|
|
return shimController
|
|
|
|
|
|
2019-08-03 17:55:46 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Display a no selection controller since we don't have any detail selected
|
2019-08-03 20:56:18 +02:00
|
|
|
|
return fullyWrappedSystemMesssageController(showButton: true)
|
2019-08-02 23:46:55 +02:00
|
|
|
|
|
2019-08-03 17:55:46 +02:00
|
|
|
|
}
|
2019-08-02 23:46:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 22:31:11 +02:00
|
|
|
|
// MARK: UINavigationControllerDelegate
|
|
|
|
|
|
|
|
|
|
extension SceneCoordinator: UINavigationControllerDelegate {
|
|
|
|
|
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
|
|
|
|
|
if rootSplitViewController.isCollapsed && viewController === masterFeedViewController {
|
|
|
|
|
activityManager.invalidateCurrentActivities()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
// MARK: Private
|
|
|
|
|
|
2019-09-01 19:43:07 +02:00
|
|
|
|
private extension SceneCoordinator {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-08-21 20:10:08 +02:00
|
|
|
|
func updateUnreadCount() {
|
|
|
|
|
var count = 0
|
|
|
|
|
for article in articles {
|
|
|
|
|
if !article.status.read {
|
|
|
|
|
count += 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
unreadCount = count
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-08 16:43:51 +02:00
|
|
|
|
func rebuildBackingStores(_ updateExpandedNodes: (() -> Void)? = nil) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
|
|
|
|
treeController.rebuild()
|
2019-09-08 16:43:51 +02:00
|
|
|
|
updateExpandedNodes?()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
rebuildShadowTable()
|
2019-08-25 18:38:04 +02:00
|
|
|
|
masterFeedViewController.reloadFeeds()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-31 18:38:03 +02:00
|
|
|
|
func rebuildShadowTable() {
|
|
|
|
|
shadowTable = [[Node]]()
|
|
|
|
|
|
|
|
|
|
for i in 0..<treeController.rootNode.numberOfChildNodes {
|
|
|
|
|
|
|
|
|
|
var result = [Node]()
|
2019-09-08 15:55:07 +02:00
|
|
|
|
let sectionNode = treeController.rootNode.childAtIndex(i)!
|
|
|
|
|
|
|
|
|
|
if expandedNodes.contains(sectionNode) {
|
|
|
|
|
for node in sectionNode.childNodes {
|
2019-08-31 18:38:03 +02:00
|
|
|
|
result.append(node)
|
|
|
|
|
if expandedNodes.contains(node) {
|
|
|
|
|
for child in node.childNodes {
|
|
|
|
|
result.append(child)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shadowTable.append(result)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
func updateShowAvatars() {
|
|
|
|
|
|
|
|
|
|
if showFeedNames {
|
|
|
|
|
self.showAvatars = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for article in articles {
|
|
|
|
|
if let authors = article.authors {
|
|
|
|
|
for author in authors {
|
|
|
|
|
if author.avatarURL != nil {
|
|
|
|
|
self.showAvatars = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.showAvatars = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-06 01:02:40 +02:00
|
|
|
|
// MARK: Select Prev Unread
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-09-06 01:02:40 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
func selectPrevUnreadArticleInTimeline() -> Bool {
|
|
|
|
|
let startingRow: Int = {
|
|
|
|
|
if let indexPath = currentArticleIndexPath {
|
|
|
|
|
return indexPath.row - 1
|
|
|
|
|
} else {
|
|
|
|
|
return articles.count - 1
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return selectPrevArticleInTimeline(startingRow: startingRow)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectPrevArticleInTimeline(startingRow: Int) -> Bool {
|
|
|
|
|
|
|
|
|
|
guard startingRow >= 0 else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i in (0...startingRow).reversed() {
|
|
|
|
|
let article = articles[i]
|
|
|
|
|
if !article.status.read {
|
|
|
|
|
selectArticle(IndexPath(row: i, section: 0))
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectPrevUnreadFeedFetcher() {
|
|
|
|
|
|
|
|
|
|
let indexPath: IndexPath = {
|
|
|
|
|
if currentFeedIndexPath == nil {
|
|
|
|
|
return IndexPath(row: 0, section: 0)
|
|
|
|
|
} else {
|
|
|
|
|
return currentFeedIndexPath!
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Increment or wrap around the IndexPath
|
|
|
|
|
let nextIndexPath: IndexPath = {
|
|
|
|
|
if indexPath.row - 1 < 0 {
|
|
|
|
|
if indexPath.section - 1 < 0 {
|
|
|
|
|
return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
|
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: shadowTable[indexPath.section - 1].count - 1, section: indexPath.section - 1)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
if selectPrevUnreadFeedFetcher(startingWith: nextIndexPath) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
|
|
|
|
|
selectPrevUnreadFeedFetcher(startingWith: maxIndexPath)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@discardableResult
|
|
|
|
|
func selectPrevUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool {
|
|
|
|
|
|
|
|
|
|
for i in (0...indexPath.section).reversed() {
|
|
|
|
|
|
|
|
|
|
let startingRow: Int = {
|
|
|
|
|
if indexPath.section == i {
|
|
|
|
|
return indexPath.row
|
|
|
|
|
} else {
|
|
|
|
|
return shadowTable[i].count - 1
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
for j in (0...startingRow).reversed() {
|
|
|
|
|
|
|
|
|
|
let prevIndexPath = IndexPath(row: j, section: i)
|
|
|
|
|
guard let node = nodeFor(prevIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {
|
|
|
|
|
assertionFailure()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if expandedNodes.contains(node) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if unreadCountProvider.unreadCount > 0 {
|
|
|
|
|
selectFeed(prevIndexPath)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Select Next Unread
|
|
|
|
|
|
2019-08-04 00:07:43 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
func selectFirstUnreadArticleInTimeline() -> Bool {
|
2019-09-06 01:02:40 +02:00
|
|
|
|
return selectNextArticleInTimeline(startingRow: 0)
|
2019-08-04 00:07:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
func selectNextUnreadArticleInTimeline() -> Bool {
|
|
|
|
|
let startingRow: Int = {
|
|
|
|
|
if let indexPath = currentArticleIndexPath {
|
2019-09-03 00:24:20 +02:00
|
|
|
|
return indexPath.row + 1
|
2019-06-29 20:35:12 +02:00
|
|
|
|
} else {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2019-09-06 01:02:40 +02:00
|
|
|
|
return selectNextArticleInTimeline(startingRow: startingRow)
|
2019-08-04 00:07:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-06 01:02:40 +02:00
|
|
|
|
func selectNextArticleInTimeline(startingRow: Int) -> Bool {
|
2019-08-04 00:07:43 +02:00
|
|
|
|
|
2019-09-03 00:24:20 +02:00
|
|
|
|
guard startingRow < articles.count else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
for i in startingRow..<articles.count {
|
|
|
|
|
let article = articles[i]
|
|
|
|
|
if !article.status.read {
|
2019-08-04 00:07:43 +02:00
|
|
|
|
selectArticle(IndexPath(row: i, section: 0))
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectNextUnreadFeedFetcher() {
|
|
|
|
|
|
2019-09-06 01:02:40 +02:00
|
|
|
|
let indexPath: IndexPath = {
|
|
|
|
|
if currentFeedIndexPath == nil {
|
|
|
|
|
return IndexPath(row: -1, section: 0)
|
|
|
|
|
} else {
|
|
|
|
|
return currentFeedIndexPath!
|
|
|
|
|
}
|
|
|
|
|
}()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
// Increment or wrap around the IndexPath
|
|
|
|
|
let nextIndexPath: IndexPath = {
|
|
|
|
|
if indexPath.row + 1 >= shadowTable[indexPath.section].count {
|
|
|
|
|
if indexPath.section + 1 >= shadowTable.count {
|
|
|
|
|
return IndexPath(row: 0, section: 0)
|
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: 0, section: indexPath.section + 1)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
if selectNextUnreadFeedFetcher(startingWith: nextIndexPath) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
selectNextUnreadFeedFetcher(startingWith: IndexPath(row: 0, section: 0))
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@discardableResult
|
|
|
|
|
func selectNextUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool {
|
|
|
|
|
|
|
|
|
|
for i in indexPath.section..<shadowTable.count {
|
|
|
|
|
|
2019-09-06 01:02:40 +02:00
|
|
|
|
let startingRow: Int = {
|
|
|
|
|
if indexPath.section == i {
|
|
|
|
|
return indexPath.row
|
|
|
|
|
} else {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
for j in startingRow..<shadowTable[indexPath.section].count {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
let nextIndexPath = IndexPath(row: j, section: i)
|
|
|
|
|
guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {
|
|
|
|
|
assertionFailure()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if expandedNodes.contains(node) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if unreadCountProvider.unreadCount > 0 {
|
2019-09-03 00:28:28 +02:00
|
|
|
|
selectFeed(nextIndexPath)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Fetching Articles
|
|
|
|
|
|
|
|
|
|
func emptyTheTimeline() {
|
|
|
|
|
if !articles.isEmpty {
|
2019-08-31 19:12:50 +02:00
|
|
|
|
replaceArticles(with: Set<Article>(), animate: true)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sortDirectionDidChange() {
|
2019-08-30 21:42:33 +02:00
|
|
|
|
replaceArticles(with: Set(articles), animate: true)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-30 21:42:33 +02:00
|
|
|
|
func replaceArticles(with unsortedArticles: Set<Article>, animate: Bool) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
|
2019-09-03 23:07:02 +02:00
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
if articles != sortedArticles {
|
2019-09-03 23:07:02 +02:00
|
|
|
|
|
2019-08-30 09:39:54 +02:00
|
|
|
|
let article = currentArticle
|
2019-06-29 20:35:12 +02:00
|
|
|
|
articles = sortedArticles
|
2019-09-03 23:07:02 +02:00
|
|
|
|
|
|
|
|
|
updateShowAvatars()
|
|
|
|
|
articleRowMap = [String: Int]()
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
|
2019-08-30 21:42:33 +02:00
|
|
|
|
masterTimelineViewController?.reloadArticles(animate: animate)
|
2019-08-30 21:17:05 +02:00
|
|
|
|
if let articleID = article?.articleID, let index = indexForArticleID(articleID) {
|
2019-08-30 09:39:54 +02:00
|
|
|
|
currentArticleIndexPath = IndexPath(row: index, section: 0)
|
|
|
|
|
}
|
2019-09-03 23:07:02 +02:00
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateArticleRowMap() {
|
|
|
|
|
var rowMap = [String: Int]()
|
|
|
|
|
var index = 0
|
|
|
|
|
articles.forEach { (article) in
|
|
|
|
|
rowMap[article.articleID] = index
|
|
|
|
|
index += 1
|
|
|
|
|
}
|
|
|
|
|
articleRowMap = rowMap
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateArticleRowMapIfNeeded() {
|
|
|
|
|
if articleRowMap.isEmpty {
|
|
|
|
|
updateArticleRowMap()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func queueFetchAndMergeArticles() {
|
2019-07-06 00:45:39 +02:00
|
|
|
|
fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func fetchAndMergeArticles() {
|
|
|
|
|
|
|
|
|
|
guard let timelineFetcher = timelineFetcher else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 22:27:53 +02:00
|
|
|
|
fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (unsortedArticles) in
|
|
|
|
|
// Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles.
|
|
|
|
|
guard let strongSelf = self else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let unsortedArticleIDs = unsortedArticles.articleIDs()
|
|
|
|
|
var updatedArticles = unsortedArticles
|
|
|
|
|
for article in strongSelf.articles {
|
|
|
|
|
if !unsortedArticleIDs.contains(article.articleID) {
|
|
|
|
|
updatedArticles.insert(article)
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
2019-08-21 22:27:53 +02:00
|
|
|
|
|
2019-08-30 21:42:33 +02:00
|
|
|
|
strongSelf.replaceArticles(with: updatedArticles, animate: true)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 22:58:55 +02:00
|
|
|
|
func cancelPendingAsyncFetches() {
|
|
|
|
|
fetchSerialNumber += 1
|
|
|
|
|
fetchRequestQueue.cancelAllRequests()
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 22:27:53 +02:00
|
|
|
|
func fetchAndReplaceArticlesSync() {
|
|
|
|
|
// To be called when the user has made a change of selection in the sidebar.
|
|
|
|
|
// It blocks the main thread, so that there’s no async delay,
|
|
|
|
|
// so that the entire display refreshes at once.
|
|
|
|
|
// It’s a better user experience this way.
|
|
|
|
|
cancelPendingAsyncFetches()
|
|
|
|
|
guard let timelineFetcher = timelineFetcher else {
|
|
|
|
|
emptyTheTimeline()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let fetchedArticles = fetchUnsortedArticlesSync(for: [timelineFetcher])
|
2019-08-30 21:42:33 +02:00
|
|
|
|
replaceArticles(with: fetchedArticles, animate: false)
|
2019-08-21 22:27:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-29 22:02:45 +02:00
|
|
|
|
func fetchAndReplaceArticlesAsync(completion: @escaping () -> Void) {
|
2019-08-21 22:58:55 +02:00
|
|
|
|
// To be called when we need to do an entire fetch, but an async delay is okay.
|
|
|
|
|
// Example: we have the Today feed selected, and the calendar day just changed.
|
|
|
|
|
cancelPendingAsyncFetches()
|
|
|
|
|
guard let timelineFetcher = timelineFetcher else {
|
|
|
|
|
emptyTheTimeline()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (articles) in
|
2019-09-03 19:12:42 +02:00
|
|
|
|
self?.replaceArticles(with: articles, animate: true)
|
2019-08-29 22:02:45 +02:00
|
|
|
|
completion()
|
2019-08-21 22:58:55 +02:00
|
|
|
|
}
|
2019-08-21 22:27:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {
|
|
|
|
|
cancelPendingAsyncFetches()
|
|
|
|
|
let articleFetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
|
|
|
|
|
if articleFetchers.isEmpty {
|
|
|
|
|
return Set<Article>()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fetchedArticles = Set<Article>()
|
|
|
|
|
for articleFetcher in articleFetchers {
|
|
|
|
|
fetchedArticles.formUnion(articleFetcher.fetchArticles())
|
|
|
|
|
}
|
|
|
|
|
return fetchedArticles
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchUnsortedArticlesAsync(for representedObjects: [Any], callback: @escaping ArticleSetBlock) {
|
|
|
|
|
// The callback will *not* be called if the fetch is no longer relevant — that is,
|
|
|
|
|
// if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called.
|
|
|
|
|
precondition(Thread.isMainThread)
|
|
|
|
|
cancelPendingAsyncFetches()
|
|
|
|
|
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in
|
|
|
|
|
precondition(Thread.isMainThread)
|
|
|
|
|
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
callback(articles)
|
|
|
|
|
}
|
|
|
|
|
fetchRequestQueue.add(fetchOperation)
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
func timelineFetcherContainsAnyPseudoFeed() -> Bool {
|
|
|
|
|
if timelineFetcher is PseudoFeed {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-29 22:02:45 +02:00
|
|
|
|
func timelineFetcherContainsAnyFolder() -> Bool {
|
|
|
|
|
if timelineFetcher is Folder {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
func timelineFetcherContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
|
|
|
|
|
|
|
|
|
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
|
|
|
|
|
|
|
|
|
if let feed = timelineFetcher as? Feed {
|
|
|
|
|
for oneFeed in feeds {
|
|
|
|
|
if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if let folder = timelineFetcher as? Folder {
|
|
|
|
|
for oneFeed in feeds {
|
|
|
|
|
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-19 22:59:46 +02:00
|
|
|
|
// MARK: Double Split
|
|
|
|
|
|
2019-09-07 16:01:29 +02:00
|
|
|
|
func installTimelineControllerIfNecessary(animated: Bool) {
|
|
|
|
|
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
|
|
|
|
|
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
|
|
|
|
|
masterTimelineViewController!.coordinator = self
|
|
|
|
|
navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: animated)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Note about the Shim Controller
|
|
|
|
|
// In the root split view controller's secondary (or detail) position we use a view controller that
|
2019-08-21 20:13:03 +02:00
|
|
|
|
// only acts as a shim (or wrapper) for the actually desired contents of the second position. This
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// is because we normally can't change the root split view controllers second position contents
|
|
|
|
|
// during the display mode change callback (in the split view controller delegate). To fool the
|
|
|
|
|
// system, we leave the same controller, the shim, in place and change its child controllers instead.
|
|
|
|
|
|
2019-09-06 14:29:36 +02:00
|
|
|
|
func installDetailController(_ detailController: UIViewController, automated: Bool) {
|
2019-09-10 01:07:49 +02:00
|
|
|
|
|
2019-09-10 14:22:32 +02:00
|
|
|
|
if let subSplit = subSplitViewController {
|
2019-09-10 01:07:49 +02:00
|
|
|
|
let controller = addNavControllerIfNecessary(detailController, showButton: false)
|
2019-09-10 14:22:32 +02:00
|
|
|
|
subSplit.showDetailViewController(controller, sender: self)
|
2019-08-02 03:06:55 +02:00
|
|
|
|
} else if rootSplitViewController.isCollapsed {
|
2019-09-10 01:07:49 +02:00
|
|
|
|
let controller = addNavControllerIfNecessary(detailController, showButton: false)
|
2019-09-06 14:29:36 +02:00
|
|
|
|
masterNavigationController.pushViewController(controller, animated: !automated)
|
2019-08-02 02:59:56 +02:00
|
|
|
|
} else {
|
2019-09-10 01:07:49 +02:00
|
|
|
|
let controller = addNavControllerIfNecessary(detailController, showButton: true)
|
2019-08-02 02:59:56 +02:00
|
|
|
|
if let shimController = rootSplitViewController.viewControllers.last {
|
|
|
|
|
shimController.replaceChildAndPinView(controller)
|
|
|
|
|
}
|
2019-09-10 01:07:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-02 02:59:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addNavControllerIfNecessary(_ controller: UIViewController, showButton: Bool) -> UIViewController {
|
|
|
|
|
if rootSplitViewController.isCollapsed {
|
2019-07-26 23:26:22 +02:00
|
|
|
|
return controller
|
|
|
|
|
} else {
|
2019-08-23 19:27:45 +02:00
|
|
|
|
let navController = ThemedNavigationController.template(rootViewController: controller)
|
2019-07-26 23:26:22 +02:00
|
|
|
|
navController.isToolbarHidden = false
|
2019-08-02 02:59:56 +02:00
|
|
|
|
if showButton {
|
|
|
|
|
controller.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
|
2019-08-03 18:44:05 +02:00
|
|
|
|
controller.navigationItem.leftItemsSupplementBackButton = true
|
2019-09-10 01:07:49 +02:00
|
|
|
|
} else {
|
|
|
|
|
controller.navigationItem.leftBarButtonItem = nil
|
|
|
|
|
controller.navigationItem.leftItemsSupplementBackButton = false
|
2019-07-26 23:26:22 +02:00
|
|
|
|
}
|
|
|
|
|
return navController
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-10 14:22:32 +02:00
|
|
|
|
func configureDoubleSplit() {
|
2019-08-02 16:12:15 +02:00
|
|
|
|
rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.30
|
2019-07-26 23:26:22 +02:00
|
|
|
|
|
2019-07-19 22:59:46 +02:00
|
|
|
|
let subSplit = UISplitViewController.template()
|
2019-07-26 23:26:22 +02:00
|
|
|
|
subSplit.preferredDisplayMode = .allVisible
|
2019-08-02 16:12:15 +02:00
|
|
|
|
subSplit.preferredPrimaryColumnWidthFraction = 0.4285
|
2019-08-02 02:59:56 +02:00
|
|
|
|
|
|
|
|
|
let shimController = UIViewController()
|
|
|
|
|
shimController.addChildAndPinView(subSplit)
|
|
|
|
|
|
|
|
|
|
rootSplitViewController.showDetailViewController(shimController, sender: self)
|
2019-07-19 22:59:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func navControllerForTimeline() -> UINavigationController {
|
2019-09-10 14:22:32 +02:00
|
|
|
|
if let subSplit = subSplitViewController {
|
2019-07-19 22:59:46 +02:00
|
|
|
|
return subSplit.viewControllers.first as! UINavigationController
|
|
|
|
|
} else {
|
|
|
|
|
return masterNavigationController
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-03 20:56:18 +02:00
|
|
|
|
func fullyWrappedSystemMesssageController(showButton: Bool) -> UIViewController {
|
2019-08-03 20:53:30 +02:00
|
|
|
|
let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
|
2019-08-03 20:56:18 +02:00
|
|
|
|
let navController = addNavControllerIfNecessary(systemMessageViewController, showButton: showButton)
|
2019-08-03 20:53:30 +02:00
|
|
|
|
let shimController = UIViewController()
|
|
|
|
|
shimController.addChildAndPinView(navController)
|
|
|
|
|
return shimController
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-02 23:46:55 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
func transitionToThreePanelMode() -> UIViewController {
|
2019-09-09 23:21:27 +02:00
|
|
|
|
|
2019-08-01 23:14:34 +02:00
|
|
|
|
defer {
|
|
|
|
|
masterNavigationController.viewControllers = [masterFeedViewController]
|
|
|
|
|
}
|
2019-09-09 23:59:24 +02:00
|
|
|
|
|
|
|
|
|
let controller: UIViewController = {
|
|
|
|
|
if let result = detailViewController {
|
|
|
|
|
return result
|
|
|
|
|
} else {
|
|
|
|
|
return UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2019-09-10 14:49:24 +02:00
|
|
|
|
configureDoubleSplit()
|
|
|
|
|
installTimelineControllerIfNecessary(animated: false)
|
|
|
|
|
|
2019-09-09 23:59:24 +02:00
|
|
|
|
// Create the new sub split controller (wrapped in the shim of course) and add the timeline in the primary position
|
2019-09-10 14:22:32 +02:00
|
|
|
|
let masterTimelineNavController = subSplitViewController!.viewControllers.first as! UINavigationController
|
2019-09-09 23:59:24 +02:00
|
|
|
|
masterTimelineNavController.viewControllers = [masterTimelineViewController!]
|
|
|
|
|
|
|
|
|
|
// Put the detail or no selection controller in the secondary (or detail) position of the sub split
|
|
|
|
|
let navController = addNavControllerIfNecessary(controller, showButton: false)
|
2019-09-10 14:22:32 +02:00
|
|
|
|
subSplitViewController!.showDetailViewController(navController, sender: self)
|
2019-09-09 23:59:24 +02:00
|
|
|
|
|
2019-09-10 15:06:43 +02:00
|
|
|
|
masterTimelineViewController!.restoreSelectionIfNecessary()
|
|
|
|
|
|
2019-09-10 14:22:32 +02:00
|
|
|
|
// Return the shim controller
|
|
|
|
|
return subSplitViewController!.parent!
|
2019-08-01 23:14:34 +02:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func transitionFromThreePanelMode() {
|
2019-09-09 23:21:27 +02:00
|
|
|
|
|
2019-08-01 23:14:34 +02:00
|
|
|
|
rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension
|
|
|
|
|
|
2019-08-02 02:59:56 +02:00
|
|
|
|
if let shimController = rootSplitViewController.viewControllers.last, let subSplit = shimController.children.first as? UISplitViewController {
|
2019-08-01 23:14:34 +02:00
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Push the timeline on to the master navigation controller. This should always be true if we have installed
|
|
|
|
|
// the sub split controller because we only install the sub split controller if a timeline needs to be displayed.
|
|
|
|
|
if let masterTimelineNav = subSplit.viewControllers.first as? UINavigationController, let masterTimeline = masterTimelineNav.topViewController {
|
2019-08-01 23:14:34 +02:00
|
|
|
|
masterNavigationController.pushViewController(masterTimeline, animated: false)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-03 20:53:30 +02:00
|
|
|
|
// Pull the detail or no selection controller out of the sub split second position and move it to the root split controller
|
|
|
|
|
// secondary (detail) position, by replacing the contents of the shim controller in the second position.
|
2019-08-02 02:59:56 +02:00
|
|
|
|
if let detailNav = subSplit.viewControllers.last as? UINavigationController, let topController = detailNav.topViewController {
|
|
|
|
|
let newNav = addNavControllerIfNecessary(topController, showButton: true)
|
|
|
|
|
shimController.replaceChildAndPinView(newNav)
|
2019-08-01 23:14:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2019-08-02 02:59:56 +02:00
|
|
|
|
|
2019-08-01 23:14:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-24 21:57:51 +02:00
|
|
|
|
// MARK: NSUserActivity
|
|
|
|
|
|
2019-08-26 00:04:15 +02:00
|
|
|
|
func updateSelectingActivity(with node: Node) {
|
|
|
|
|
switch true {
|
|
|
|
|
case node.representedObject === SmartFeedsController.shared.todayFeed:
|
2019-09-01 02:30:21 +02:00
|
|
|
|
activityManager.selectingToday()
|
2019-08-26 00:04:15 +02:00
|
|
|
|
case node.representedObject === SmartFeedsController.shared.unreadFeed:
|
2019-09-01 02:30:21 +02:00
|
|
|
|
activityManager.selectingAllUnread()
|
2019-08-26 00:04:15 +02:00
|
|
|
|
case node.representedObject === SmartFeedsController.shared.starredFeed:
|
2019-09-01 02:30:21 +02:00
|
|
|
|
activityManager.selectingStarred()
|
2019-08-27 21:20:34 +02:00
|
|
|
|
case node.representedObject is Folder:
|
2019-09-01 02:30:21 +02:00
|
|
|
|
activityManager.selectingFolder(node.representedObject as! Folder)
|
2019-08-27 21:20:34 +02:00
|
|
|
|
case node.representedObject is Feed:
|
2019-09-01 02:30:21 +02:00
|
|
|
|
activityManager.selectingFeed(node.representedObject as! Feed)
|
2019-08-26 00:04:15 +02:00
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleSelectToday() {
|
2019-08-26 00:08:39 +02:00
|
|
|
|
if let indexPath = indexPathFor(SmartFeedsController.shared.todayFeed) {
|
2019-08-26 00:04:15 +02:00
|
|
|
|
selectFeed(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleSelectAllUnread() {
|
2019-08-26 00:08:39 +02:00
|
|
|
|
if let indexPath = indexPathFor(SmartFeedsController.shared.unreadFeed) {
|
2019-08-26 00:04:15 +02:00
|
|
|
|
selectFeed(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleSelectStarred() {
|
2019-08-26 00:08:39 +02:00
|
|
|
|
if let indexPath = indexPathFor(SmartFeedsController.shared.starredFeed) {
|
2019-08-26 00:04:15 +02:00
|
|
|
|
selectFeed(indexPath)
|
|
|
|
|
}
|
2019-08-24 21:57:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-28 00:43:15 +02:00
|
|
|
|
func handleSelectFolder(_ activity: NSUserActivity) {
|
|
|
|
|
guard let accountNode = findAccountNode(for: activity), let folderNode = findFolderNode(for: activity, beginningAt: accountNode) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if let indexPath = indexPathFor(folderNode) {
|
|
|
|
|
selectFeed(indexPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleSelectFeed(_ activity: NSUserActivity) {
|
|
|
|
|
guard let accountNode = findAccountNode(for: activity), let feedNode = findFeedNode(for: activity, beginningAt: accountNode) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if let feed = feedNode.representedObject as? Feed {
|
|
|
|
|
discloseFeed(feed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-25 02:31:29 +02:00
|
|
|
|
func handleReadArticle(_ activity: NSUserActivity) {
|
|
|
|
|
guard let accountNode = findAccountNode(for: activity), let feedNode = findFeedNode(for: activity, beginningAt: accountNode) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 03:23:14 +02:00
|
|
|
|
discloseFeed(feedNode.representedObject as! Feed) {
|
2019-08-25 02:31:29 +02:00
|
|
|
|
|
2019-09-01 03:23:14 +02:00
|
|
|
|
guard let articleID = activity.userInfo?[ActivityID.articleID.rawValue] as? String else { return }
|
2019-08-25 02:31:29 +02:00
|
|
|
|
|
2019-09-01 03:23:14 +02:00
|
|
|
|
for (index, article) in self.articles.enumerated() {
|
|
|
|
|
if article.articleID == articleID {
|
|
|
|
|
self.selectArticle(IndexPath(row: index, section: 0))
|
|
|
|
|
break
|
|
|
|
|
}
|
2019-08-25 02:31:29 +02:00
|
|
|
|
}
|
2019-09-01 03:23:14 +02:00
|
|
|
|
|
2019-08-25 02:31:29 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findAccountNode(for activity: NSUserActivity) -> Node? {
|
|
|
|
|
guard let accountID = activity.userInfo?[ActivityID.accountID.rawValue] as? String else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let accountName = activity.userInfo?[ActivityID.accountName.rawValue] as? String else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.name == accountName }) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-28 00:43:15 +02:00
|
|
|
|
func findFolderNode(for activity: NSUserActivity, beginningAt startingNode: Node) -> Node? {
|
|
|
|
|
guard let folderName = activity.userInfo?[ActivityID.folderName.rawValue] as? String else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Folder)?.nameForDisplay == folderName }) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-25 02:31:29 +02:00
|
|
|
|
func findFeedNode(for activity: NSUserActivity, beginningAt startingNode: Node) -> Node? {
|
|
|
|
|
guard let feedID = activity.userInfo?[ActivityID.feedID.rawValue] as? String else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|