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
|
2019-10-03 16:53:21 +02:00
|
|
|
|
import UserNotifications
|
2019-06-29 20:35:12 +02:00
|
|
|
|
import Account
|
|
|
|
|
import Articles
|
|
|
|
|
import RSCore
|
|
|
|
|
import RSTree
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
enum PanelMode {
|
|
|
|
|
case unset
|
|
|
|
|
case three
|
|
|
|
|
case standard
|
|
|
|
|
}
|
2019-08-31 22:53:47 +02:00
|
|
|
|
enum SearchScope: Int {
|
|
|
|
|
case timeline = 0
|
|
|
|
|
case global = 1
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-18 14:53:56 +02:00
|
|
|
|
enum ShowFeedName {
|
|
|
|
|
case none
|
|
|
|
|
case byline
|
|
|
|
|
case feed
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-30 01:39:40 +01:00
|
|
|
|
lazy var webViewProvider = WebViewProvider(coordinator: self, viewController: rootSplitViewController)
|
2020-01-27 20:58:32 +01:00
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
private var panelMode: PanelMode = .unset
|
|
|
|
|
|
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-11-20 23:41:13 +01:00
|
|
|
|
private var subSplitViewController: UISplitViewController?
|
2019-09-10 14:22:32 +02:00
|
|
|
|
|
2019-09-24 11:29:15 +02:00
|
|
|
|
private var articleViewController: ArticleViewController? {
|
|
|
|
|
if let detail = masterNavigationController.viewControllers.last as? ArticleViewController {
|
2019-08-02 23:46:55 +02:00
|
|
|
|
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 {
|
2019-09-24 11:29:15 +02:00
|
|
|
|
return navController.topViewController as? ArticleViewController
|
2019-08-02 11:22:47 +02:00
|
|
|
|
}
|
2019-08-02 02:59:56 +02:00
|
|
|
|
} else {
|
2019-09-11 01:26:27 +02:00
|
|
|
|
if let navController = rootSplitViewController.viewControllers.last as? UINavigationController {
|
2019-09-24 11:29:15 +02:00
|
|
|
|
return navController.topViewController as? ArticleViewController
|
2019-08-02 11:22:47 +02:00
|
|
|
|
}
|
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-11-20 23:41:13 +01:00
|
|
|
|
private var wasRootSplitViewControllerCollapsed = false
|
|
|
|
|
|
2019-07-06 00:45:39 +02:00
|
|
|
|
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
2020-03-28 00:24:57 +01:00
|
|
|
|
private let rebuildBackingStoresQueue = CoalescingQueue(name: "Rebuild The Backing Stores", interval: 1.0)
|
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 animatingChanges = false
|
2019-11-25 01:29:00 +01:00
|
|
|
|
private var expandedTable = Set<ContainerIdentifier>()
|
2019-11-27 18:43:36 +01:00
|
|
|
|
private var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
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-11-19 18:16:43 +01:00
|
|
|
|
private var savedSearchArticles: ArticleArray? = nil
|
|
|
|
|
private var savedSearchArticleIds: Set<String>? = nil
|
2019-11-19 02:12:24 +01:00
|
|
|
|
var isTimelineViewControllerPending = false
|
|
|
|
|
var isArticleViewControllerPending = false
|
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 {
|
2019-09-13 15:29:56 +02:00
|
|
|
|
sortParametersDidChange()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-14 23:29:20 +01:00
|
|
|
|
|
2019-09-09 00:41:00 +02:00
|
|
|
|
private(set) var groupByFeed = AppDefaults.timelineGroupByFeed {
|
|
|
|
|
didSet {
|
|
|
|
|
if groupByFeed != oldValue {
|
2019-09-13 15:29:56 +02:00
|
|
|
|
sortParametersDidChange()
|
2019-09-09 00:41:00 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-10-08 02:33:30 +02:00
|
|
|
|
|
2019-11-19 02:12:24 +01:00
|
|
|
|
var prefersStatusBarHidden = false
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
private let treeControllerDelegate = WebFeedTreeControllerDelegate()
|
2019-11-13 22:22:22 +01:00
|
|
|
|
private let treeController: TreeController
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-11-26 23:33:11 +01:00
|
|
|
|
var stateRestorationActivity: NSUserActivity {
|
|
|
|
|
let activity = activityManager.stateRestorationActivity
|
|
|
|
|
var userInfo = activity.userInfo == nil ? [AnyHashable: Any]() : activity.userInfo
|
|
|
|
|
userInfo![UserInfoKey.windowState] = windowState()
|
|
|
|
|
activity.userInfo = userInfo
|
|
|
|
|
return activity
|
2019-09-01 02:30:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
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-11-20 23:41:13 +01:00
|
|
|
|
return panelMode == .three
|
2019-07-27 21:49:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 03:23:12 +01:00
|
|
|
|
var isReadFeedsFiltered: Bool {
|
2019-11-22 17:55:54 +01:00
|
|
|
|
return treeControllerDelegate.isReadFiltered
|
2019-11-21 22:55:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 18:43:36 +01:00
|
|
|
|
var isReadArticlesFiltered: Bool {
|
|
|
|
|
if let feedID = timelineFeed?.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
|
|
|
|
return readFilterEnabled
|
|
|
|
|
} else {
|
|
|
|
|
return timelineDefaultReadFilterType != .none
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var timelineDefaultReadFilterType: ReadFilterType {
|
|
|
|
|
return timelineFeed?.defaultReadFilterType ?? .none
|
|
|
|
|
}
|
2019-11-22 01:22:43 +01:00
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
var rootNode: Node {
|
|
|
|
|
return treeController.rootNode
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-30 01:06:35 +01:00
|
|
|
|
// At some point we should refactor the current Feed IndexPath out and only use the timeline feed
|
2019-09-05 04:06:29 +02:00
|
|
|
|
private(set) var currentFeedIndexPath: IndexPath?
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-11-06 01:05:57 +01:00
|
|
|
|
var timelineIconImage: IconImage? {
|
2019-11-15 13:19:14 +01:00
|
|
|
|
if let feed = timelineFeed as? WebFeed {
|
2019-10-29 03:33:13 +01:00
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: feed)
|
2019-10-29 03:33:13 +01:00
|
|
|
|
if feedIconImage != nil {
|
|
|
|
|
return feedIconImage
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-06 01:05:57 +01:00
|
|
|
|
if let faviconIconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
|
|
|
|
|
return faviconIconImage
|
2019-10-29 03:33:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
|
return (timelineFeed as? SmallIconProvider)?.smallIcon
|
2019-09-22 00:59:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-29 21:31:15 +01:00
|
|
|
|
private var exceptionArticleFetcher: ArticleFetcher?
|
2019-11-19 18:16:43 +01:00
|
|
|
|
private(set) var timelineFeed: Feed?
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-11-11 23:59:42 +01:00
|
|
|
|
var timelineMiddleIndexPath: IndexPath?
|
|
|
|
|
|
2020-04-18 14:53:56 +02:00
|
|
|
|
private(set) var showFeedNames = ShowFeedName.none
|
2019-11-06 01:05:57 +01:00
|
|
|
|
private(set) var showIcons = false
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-09-05 04:06:29 +02:00
|
|
|
|
var prevFeedIndexPath: IndexPath? {
|
2020-01-31 02:37:22 +01:00
|
|
|
|
guard let indexPath = currentFeedIndexPath else {
|
2019-09-05 04:06:29 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-31 02:37:22 +01:00
|
|
|
|
let prevIndexPath: IndexPath? = {
|
2019-09-05 04:06:29 +02:00
|
|
|
|
if indexPath.row - 1 < 0 {
|
2020-01-31 02:37:22 +01:00
|
|
|
|
for i in (0..<indexPath.section).reversed() {
|
|
|
|
|
if shadowTable[i].count > 0 {
|
|
|
|
|
return IndexPath(row: shadowTable[i].count - 1, section: i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2019-09-05 04:06:29 +02:00
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return prevIndexPath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nextFeedIndexPath: IndexPath? {
|
2020-01-31 02:37:22 +01:00
|
|
|
|
guard let indexPath = currentFeedIndexPath else {
|
2019-09-05 04:06:29 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-31 02:37:22 +01:00
|
|
|
|
let nextIndexPath: IndexPath? = {
|
2019-09-05 04:06:29 +02:00
|
|
|
|
if indexPath.row + 1 >= shadowTable[indexPath.section].count {
|
2020-01-31 02:37:22 +01:00
|
|
|
|
for i in indexPath.section + 1..<shadowTable.count {
|
|
|
|
|
if shadowTable[i].count > 0 {
|
|
|
|
|
return IndexPath(row: 0, section: i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2019-09-05 04:06:29 +02:00
|
|
|
|
} else {
|
|
|
|
|
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return nextIndexPath
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
var isPrevArticleAvailable: Bool {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
guard let articleRow = currentArticleRow else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return false
|
|
|
|
|
}
|
2019-09-11 16:11:33 +02:00
|
|
|
|
return articleRow > 0
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isNextArticleAvailable: Bool {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
guard let articleRow = currentArticleRow else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return false
|
|
|
|
|
}
|
2019-09-11 16:11:33 +02:00
|
|
|
|
return articleRow + 1 < articles.count
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
var prevArticle: Article? {
|
|
|
|
|
guard isPrevArticleAvailable, let articleRow = currentArticleRow else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-09-11 16:45:48 +02:00
|
|
|
|
return articles[articleRow - 1]
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
var nextArticle: Article? {
|
|
|
|
|
guard isNextArticleAvailable, let articleRow = currentArticleRow else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-09-11 16:45:48 +02:00
|
|
|
|
return articles[articleRow + 1]
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var firstUnreadArticleIndexPath: IndexPath? {
|
|
|
|
|
for (row, article) in articles.enumerated() {
|
|
|
|
|
if !article.status.read {
|
|
|
|
|
return IndexPath(row: row, section: 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
var currentArticle: Article?
|
2019-09-11 16:16:04 +02:00
|
|
|
|
|
2020-01-08 19:06:41 +01:00
|
|
|
|
private(set) var articles = ArticleArray() {
|
|
|
|
|
didSet {
|
|
|
|
|
timelineMiddleIndexPath = nil
|
2020-01-27 20:58:32 +01:00
|
|
|
|
articleDictionaryNeedsUpdate = true
|
2020-01-08 19:06:41 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-27 20:58:32 +01:00
|
|
|
|
|
|
|
|
|
private var articleDictionaryNeedsUpdate = true
|
|
|
|
|
private var _idToArticleDictionary = [String: Article]()
|
|
|
|
|
private var idToAticleDictionary: [String: Article] {
|
|
|
|
|
if articleDictionaryNeedsUpdate {
|
|
|
|
|
rebuildArticleDictionaries()
|
|
|
|
|
}
|
|
|
|
|
return _idToArticleDictionary
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 16:15:22 +02:00
|
|
|
|
private var currentArticleRow: Int? {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
guard let article = currentArticle else { return nil }
|
|
|
|
|
return articles.firstIndex(of: article)
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
var isTimelineUnreadAvailable: Bool {
|
2019-12-14 23:29:20 +01:00
|
|
|
|
return unreadCount > 0
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-11-13 22:22:22 +01:00
|
|
|
|
treeController = TreeController(delegate: treeControllerDelegate)
|
|
|
|
|
|
2019-07-19 19:29:17 +02:00
|
|
|
|
super.init()
|
|
|
|
|
|
2019-11-25 01:29:00 +01:00
|
|
|
|
for sectionNode in treeController.rootNode.childNodes {
|
|
|
|
|
markExpanded(sectionNode)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
shadowTable.append([Node]())
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 03:23:12 +01:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
|
2020-01-20 00:44:13 +01:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
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)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, 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)
|
2020-02-03 23:05:22 +01:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
|
|
|
|
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-11-19 02:12:24 +01:00
|
|
|
|
rootSplitViewController.viewControllers = [InteractiveNavigationController.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-24 11:29:15 +02:00
|
|
|
|
let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
|
|
|
|
|
articleViewController.coordinator = self
|
2019-11-12 23:13:59 +01:00
|
|
|
|
let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: true)
|
2019-09-24 02:23:23 +02:00
|
|
|
|
rootSplitViewController.showDetailViewController(detailNavigationController, sender: self)
|
2019-07-26 23:26:22 +02:00
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
configurePanelMode(for: size)
|
2019-09-09 23:59:24 +02:00
|
|
|
|
|
2019-07-19 22:59:46 +02:00
|
|
|
|
return rootSplitViewController
|
2019-07-06 00:45:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 03:23:12 +01:00
|
|
|
|
func restoreWindowState(_ activity: NSUserActivity?) {
|
|
|
|
|
if let activity = activity, let windowState = activity.userInfo?[UserInfoKey.windowState] as? [AnyHashable: Any] {
|
2019-11-27 21:52:40 +01:00
|
|
|
|
|
|
|
|
|
if let containerExpandedWindowState = windowState[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: AnyHashable]] {
|
|
|
|
|
let containerIdentifers = containerExpandedWindowState.compactMap( { ContainerIdentifier(userInfo: $0) })
|
|
|
|
|
expandedTable = Set(containerIdentifers)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let readArticlesFilterState = windowState[UserInfoKey.readArticlesFilterState] as? [[AnyHashable: AnyHashable]: Bool] {
|
|
|
|
|
for key in readArticlesFilterState.keys {
|
|
|
|
|
if let feedIdentifier = FeedIdentifier(userInfo: key) {
|
|
|
|
|
readFilterEnabledTable[feedIdentifier] = readArticlesFilterState[key]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-11-28 02:54:52 +01:00
|
|
|
|
|
|
|
|
|
rebuildBackingStores(initialLoad: true)
|
|
|
|
|
|
2019-11-27 21:52:40 +01:00
|
|
|
|
// You can't assign the Feeds Read Filter until we've built the backing stores at least once or there is nothing
|
|
|
|
|
// for state restoration to work with while we are waiting for the unread counts to initialize.
|
|
|
|
|
if let readFeedsFilterState = windowState[UserInfoKey.readFeedsFilterState] as? Bool {
|
|
|
|
|
treeControllerDelegate.isReadFiltered = readFeedsFilterState
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
rebuildBackingStores(initialLoad: true)
|
|
|
|
|
|
2019-11-26 23:33:11 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func handle(_ activity: NSUserActivity) {
|
|
|
|
|
selectFeed(indexPath: nil) {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
guard let activityType = ActivityType(rawValue: activity.activityType) else { return }
|
|
|
|
|
switch activityType {
|
2019-11-26 23:33:11 +01:00
|
|
|
|
case .restoration:
|
|
|
|
|
break
|
2019-11-19 18:16:43 +01:00
|
|
|
|
case .selectFeed:
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.handleSelectFeed(activity.userInfo)
|
2019-11-19 18:16:43 +01:00
|
|
|
|
case .nextUnread:
|
|
|
|
|
self.selectFirstUnreadInAllUnread()
|
|
|
|
|
case .readArticle:
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.handleReadArticle(activity.userInfo)
|
2019-11-19 18:16:43 +01:00
|
|
|
|
case .addFeedIntent:
|
|
|
|
|
self.showAdd(.feed)
|
|
|
|
|
}
|
2019-08-25 02:31:29 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func handle(_ response: UNNotificationResponse) {
|
2019-10-03 16:53:21 +02:00
|
|
|
|
let userInfo = response.notification.request.content.userInfo
|
2020-02-18 02:40:40 +01:00
|
|
|
|
handleReadArticle(userInfo)
|
2019-10-03 16:53:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
func configurePanelMode(for size: CGSize) {
|
|
|
|
|
guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad else {
|
2019-09-09 23:59:24 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-20 23:41:13 +01:00
|
|
|
|
|
2019-11-20 00:20:24 +01:00
|
|
|
|
if (size.width / size.height) > 1.2 {
|
2019-11-20 23:41:13 +01:00
|
|
|
|
if panelMode == .unset || panelMode == .standard {
|
|
|
|
|
panelMode = .three
|
|
|
|
|
configureThreePanelMode()
|
2019-09-09 23:59:24 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2019-11-20 23:41:13 +01:00
|
|
|
|
if panelMode == .unset || panelMode == .three {
|
|
|
|
|
panelMode = .standard
|
|
|
|
|
configureStandardPanelMode()
|
2019-09-09 23:59:24 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-11-20 23:41:13 +01:00
|
|
|
|
|
|
|
|
|
wasRootSplitViewControllerCollapsed = rootSplitViewController.isCollapsed
|
2019-09-09 23:59:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-29 00:16:34 +02:00
|
|
|
|
func resetFocus() {
|
|
|
|
|
if currentArticle != nil {
|
|
|
|
|
masterTimelineViewController?.focus()
|
|
|
|
|
} else {
|
|
|
|
|
masterFeedViewController?.focus()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-02 00:41:46 +02:00
|
|
|
|
func selectFirstUnreadInAllUnread() {
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(SmartFeedsController.shared)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.unreadFeed) {
|
|
|
|
|
self.selectFeed(SmartFeedsController.shared.unreadFeed) {
|
2020-01-26 00:13:33 +01:00
|
|
|
|
self.selectFirstUnreadArticleInTimeline()
|
|
|
|
|
}
|
2019-11-19 18:16:43 +01:00
|
|
|
|
}
|
2019-09-02 00:41:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func showSearch() {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
selectFeed(indexPath: nil) {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
self.installTimelineControllerIfNecessary(animated: false)
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
|
|
|
|
self.masterTimelineViewController!.showSearchAll()
|
|
|
|
|
}
|
2019-09-06 17:29:00 +02:00
|
|
|
|
}
|
2019-09-02 00:41:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
// MARK: Notifications
|
|
|
|
|
|
2019-11-27 03:23:12 +01:00
|
|
|
|
@objc func unreadCountDidInitialize(_ notification: Notification) {
|
|
|
|
|
guard notification.object is AccountManager else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if isReadFeedsFiltered {
|
2019-11-28 03:08:03 +01:00
|
|
|
|
rebuildBackingStores()
|
2019-11-27 03:23:12 +01:00
|
|
|
|
}
|
2019-11-28 19:40:33 +01:00
|
|
|
|
treeControllerDelegate.resetFilterExceptions()
|
2019-11-27 03:23:12 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-20 00:44:13 +01:00
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
2020-01-29 20:19:28 +01:00
|
|
|
|
// We will handle the filtering of unread feeds in unreadCountDidInitialize after they have all be calculated
|
|
|
|
|
guard AccountManager.shared.isUnreadCountsInitialized else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-28 07:43:54 +01:00
|
|
|
|
queueRebuildBackingStores()
|
2020-01-20 00:44:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
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() {
|
2020-01-24 02:07:20 +01:00
|
|
|
|
fetchAndMergeArticlesAsync(animated: true) {
|
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
|
|
|
|
|
self.rebuildBackingStores()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
rebuildBackingStores()
|
2019-08-29 22:02:45 +02:00
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func batchUpdateDidPerform(_ notification: Notification) {
|
2020-03-28 00:24:57 +01:00
|
|
|
|
rebuildBackingStores()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func displayNameDidChange(_ note: Notification) {
|
|
|
|
|
rebuildBackingStores()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func accountStateDidChange(_ note: Notification) {
|
2019-11-25 15:14:33 +01:00
|
|
|
|
let expandNewlyActivatedAccount = {
|
|
|
|
|
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
|
|
|
|
|
account.isActive,
|
|
|
|
|
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
|
|
|
|
self.markExpanded(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 01:29:00 +01:00
|
|
|
|
if timelineFetcherContainsAnyPseudoFeed() {
|
2020-01-24 02:07:20 +01:00
|
|
|
|
fetchAndMergeArticlesAsync(animated: true) {
|
2019-12-09 02:14:33 +01:00
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.rebuildBackingStores(updateExpandedNodes: expandNewlyActivatedAccount)
|
2019-11-25 01:29:00 +01:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.rebuildBackingStores(updateExpandedNodes: expandNewlyActivatedAccount)
|
2019-11-25 01:29:00 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-08 16:43:51 +02:00
|
|
|
|
@objc func userDidAddAccount(_ note: Notification) {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
let expandNewAccount = {
|
|
|
|
|
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
|
|
|
|
|
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
|
|
|
|
self.markExpanded(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if timelineFetcherContainsAnyPseudoFeed() {
|
2020-01-24 02:07:20 +01:00
|
|
|
|
fetchAndMergeArticlesAsync(animated: true) {
|
2019-12-09 02:14:33 +01:00
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.rebuildBackingStores(updateExpandedNodes: expandNewAccount)
|
2019-11-25 01:29:00 +01:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.rebuildBackingStores(updateExpandedNodes: expandNewAccount)
|
2019-11-25 01:29:00 +01:00
|
|
|
|
}
|
2019-09-08 16:43:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func userDidDeleteAccount(_ note: Notification) {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
let cleanupAccount = {
|
|
|
|
|
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
|
|
|
|
|
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
|
|
|
|
self.unmarkExpanded(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if timelineFetcherContainsAnyPseudoFeed() {
|
2020-01-24 02:07:20 +01:00
|
|
|
|
fetchAndMergeArticlesAsync(animated: true) {
|
2019-12-09 02:14:33 +01:00
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.rebuildBackingStores(updateExpandedNodes: cleanupAccount)
|
2019-11-25 01:29:00 +01:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.rebuildBackingStores(updateExpandedNodes: cleanupAccount)
|
2019-11-25 01:29:00 +01:00
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
@objc func userDidAddFeed(_ notification: Notification) {
|
|
|
|
|
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
discloseWebFeed(webFeed, animations: [.scroll, .navigation])
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
@objc func userDefaultsDidChange(_ note: Notification) {
|
|
|
|
|
self.sortDirection = AppDefaults.timelineSortDirection
|
2019-09-09 00:41:00 +02:00
|
|
|
|
self.groupByFeed = AppDefaults.timelineGroupByFeed
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func accountDidDownloadArticles(_ note: Notification) {
|
2019-11-15 03:11:41 +01:00
|
|
|
|
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let shouldFetchAndMergeArticles = timelineFetcherContainsAnyFeed(feeds) || timelineFetcherContainsAnyPseudoFeed()
|
|
|
|
|
if shouldFetchAndMergeArticles {
|
|
|
|
|
queueFetchAndMergeArticles()
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-03 23:05:22 +01:00
|
|
|
|
|
|
|
|
|
@objc func willEnterForeground(_ note: Notification) {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
// Don't interfere with any fetch requests that we may have initiated before the app was returned to the foreground.
|
|
|
|
|
// For example if you select Next Unread from the Home Screen Quick actions, you can start a request before we are
|
|
|
|
|
// in the foreground.
|
|
|
|
|
if !fetchRequestQueue.isAnyCurrentRequest {
|
|
|
|
|
queueFetchAndMergeArticles()
|
|
|
|
|
}
|
2020-02-03 23:05:22 +01:00
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
// MARK: API
|
|
|
|
|
|
2019-12-02 21:14:35 +01:00
|
|
|
|
func suspend() {
|
|
|
|
|
fetchAndMergeArticlesQueue.performCallsImmediately()
|
2020-03-28 00:24:57 +01:00
|
|
|
|
rebuildBackingStoresQueue.performCallsImmediately()
|
2019-12-02 21:14:35 +01:00
|
|
|
|
fetchRequestQueue.cancelAllRequests()
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-24 22:00:01 +01:00
|
|
|
|
func cleanUp(conditional: Bool) {
|
2020-03-11 21:47:00 +01:00
|
|
|
|
if isReadFeedsFiltered {
|
|
|
|
|
rebuildBackingStores()
|
|
|
|
|
}
|
2020-03-24 22:00:01 +01:00
|
|
|
|
if isReadArticlesFiltered && (AppDefaults.refreshClearsReadArticles || !conditional) {
|
2020-03-11 21:47:00 +01:00
|
|
|
|
refreshTimeline(resetScroll: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-22 16:18:07 +01:00
|
|
|
|
func toggleReadFeedsFilter() {
|
|
|
|
|
if isReadFeedsFiltered {
|
|
|
|
|
treeControllerDelegate.isReadFiltered = false
|
|
|
|
|
} else {
|
|
|
|
|
treeControllerDelegate.isReadFiltered = true
|
|
|
|
|
}
|
|
|
|
|
rebuildBackingStores()
|
|
|
|
|
masterFeedViewController?.updateUI()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toggleReadArticlesFilter() {
|
|
|
|
|
guard let feedID = timelineFeed?.feedID else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isReadArticlesFiltered {
|
|
|
|
|
readFilterEnabledTable[feedID] = false
|
|
|
|
|
} else {
|
|
|
|
|
readFilterEnabledTable[feedID] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshTimeline(resetScroll: false)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 12:33:54 +02:00
|
|
|
|
func shadowNodesFor(section: Int) -> [Node] {
|
2019-08-29 01:06:27 +02:00
|
|
|
|
return shadowTable[section]
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-27 20:58:32 +01:00
|
|
|
|
func articleFor(_ articleID: String) -> Article? {
|
|
|
|
|
return idToAticleDictionary[articleID]
|
|
|
|
|
}
|
|
|
|
|
|
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-08-21 20:10:08 +02:00
|
|
|
|
func unreadCountFor(_ node: Node) -> Int {
|
2020-01-16 23:24:48 +01:00
|
|
|
|
// The coordinator supplies the unread count for the currently selected feed
|
|
|
|
|
if let feed = timelineFeed, let selectedNode = rootNode.descendantNodeRepresentingObject(feed as AnyObject), selectedNode == node {
|
2019-08-21 20:10:08 +02:00
|
|
|
|
return unreadCount
|
|
|
|
|
}
|
|
|
|
|
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
|
|
|
|
|
return unreadCountProvider.unreadCount
|
|
|
|
|
}
|
2020-01-16 23:24:48 +01:00
|
|
|
|
assertionFailure("This method should only be called for nodes that have an UnreadCountProvider as the represented object.")
|
2019-08-21 20:10:08 +02:00
|
|
|
|
return 0
|
|
|
|
|
}
|
2019-11-21 22:55:50 +01:00
|
|
|
|
|
2019-12-09 02:14:33 +01:00
|
|
|
|
func refreshTimeline(resetScroll: Bool) {
|
2020-02-18 23:14:18 +01:00
|
|
|
|
if let article = self.currentArticle, let account = article.account {
|
|
|
|
|
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID)
|
|
|
|
|
}
|
2019-11-24 17:27:02 +01:00
|
|
|
|
fetchAndReplaceArticlesAsync(animated: true) {
|
2019-12-09 02:14:33 +01:00
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles(resetScroll: resetScroll)
|
2019-11-22 03:31:58 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func isExpanded(_ containerIdentifiable: ContainerIdentifiable) -> Bool {
|
|
|
|
|
if let containerID = containerIdentifiable.containerID {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
return expandedTable.contains(containerID)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2019-08-21 20:10:08 +02:00
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func isExpanded(_ node: Node) -> Bool {
|
|
|
|
|
if let containerIdentifiable = node.representedObject as? ContainerIdentifiable {
|
|
|
|
|
return isExpanded(containerIdentifiable)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 18:30:58 +01:00
|
|
|
|
func expand(_ node: Node) {
|
|
|
|
|
guard let containerIdentifiable = node.representedObject as? ContainerIdentifiable else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
markExpanded(containerIdentifiable)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = true
|
2019-09-11 23:53:27 +02:00
|
|
|
|
rebuildShadowTable()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:38:33 +02:00
|
|
|
|
func expandAllSectionsAndFolders() {
|
2019-09-11 23:53:27 +02:00
|
|
|
|
for sectionNode in treeController.rootNode.childNodes {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
markExpanded(sectionNode)
|
2019-09-05 23:38:33 +02:00
|
|
|
|
for topLevelNode in sectionNode.childNodes {
|
2019-09-11 23:53:27 +02:00
|
|
|
|
if topLevelNode.representedObject is Folder {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
markExpanded(topLevelNode)
|
2019-09-05 23:38:33 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = true
|
2019-09-11 23:53:27 +02:00
|
|
|
|
rebuildShadowTable()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 23:53:27 +02:00
|
|
|
|
func collapse(_ node: Node) {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
unmarkExpanded(node)
|
2019-09-05 23:38:33 +02:00
|
|
|
|
animatingChanges = true
|
2019-09-11 23:53:27 +02:00
|
|
|
|
rebuildShadowTable()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = false
|
2020-01-27 05:18:43 +01:00
|
|
|
|
clearTimelineIfNoLongerAvailable()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 23:38:33 +02:00
|
|
|
|
func collapseAllFolders() {
|
|
|
|
|
for sectionNode in treeController.rootNode.childNodes {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
unmarkExpanded(sectionNode)
|
2019-09-05 23:38:33 +02:00
|
|
|
|
for topLevelNode in sectionNode.childNodes {
|
2019-09-11 23:53:27 +02:00
|
|
|
|
if topLevelNode.representedObject is Folder {
|
2019-11-25 01:29:00 +01:00
|
|
|
|
unmarkExpanded(topLevelNode)
|
2019-09-05 23:38:33 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = true
|
2019-09-11 23:53:27 +02:00
|
|
|
|
rebuildShadowTable()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
animatingChanges = false
|
2020-01-27 05:18:43 +01:00
|
|
|
|
clearTimelineIfNoLongerAvailable()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-03 20:59:22 +02:00
|
|
|
|
func masterFeedIndexPathForCurrentTimeline() -> IndexPath? {
|
2019-11-15 13:19:14 +01:00
|
|
|
|
guard let node = treeController.rootNode.descendantNodeRepresentingObject(timelineFeed as AnyObject) else {
|
2019-09-03 20:59:22 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return indexPathFor(node)
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func selectFeed(_ feed: Feed?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
|
|
|
|
|
let indexPath: IndexPath? = {
|
|
|
|
|
if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) {
|
|
|
|
|
return indexPath
|
|
|
|
|
} else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
selectFeed(indexPath: indexPath, animations: animations, deselectArticle: deselectArticle, completion: completion)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectFeed(indexPath: IndexPath?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
guard indexPath != currentFeedIndexPath else {
|
|
|
|
|
completion?()
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-09-13 18:19:19 +02:00
|
|
|
|
|
2019-09-05 04:06:29 +02:00
|
|
|
|
currentFeedIndexPath = indexPath
|
2020-01-28 05:57:52 +01:00
|
|
|
|
masterFeedViewController.updateFeedSelection(animations: animations)
|
2019-09-06 20:45:45 +02:00
|
|
|
|
|
2019-11-24 20:37:56 +01:00
|
|
|
|
if deselectArticle {
|
|
|
|
|
selectArticle(nil)
|
|
|
|
|
}
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
|
if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
|
|
|
|
self.activityManager.selecting(feed: feed)
|
2020-01-28 05:57:52 +01:00
|
|
|
|
self.installTimelineControllerIfNecessary(animated: animations.contains(.navigation))
|
2019-11-24 17:27:02 +01:00
|
|
|
|
setTimelineFeed(feed, animated: false) {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
completion?()
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-02 00:41:46 +02:00
|
|
|
|
} else {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
2019-11-24 17:27:02 +01:00
|
|
|
|
setTimelineFeed(nil, animated: false) {
|
2020-03-28 07:43:54 +01:00
|
|
|
|
if self.isReadFeedsFiltered {
|
|
|
|
|
self.queueRebuildBackingStores()
|
|
|
|
|
}
|
2019-11-19 18:16:43 +01:00
|
|
|
|
self.activityManager.invalidateSelecting()
|
|
|
|
|
if self.rootSplitViewController.isCollapsed && self.navControllerForTimeline().viewControllers.last is MasterTimelineViewController {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
self.navControllerForTimeline().popViewController(animated: animations.contains(.navigation))
|
2019-11-19 18:16:43 +01:00
|
|
|
|
}
|
|
|
|
|
completion?()
|
2019-09-06 14:29:36 +02:00
|
|
|
|
}
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
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 {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
selectFeed(indexPath: indexPath, animations: [.navigation, .scroll])
|
2019-09-05 04:06:29 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectNextFeed() {
|
|
|
|
|
if let indexPath = nextFeedIndexPath {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
selectFeed(indexPath: indexPath, animations: [.navigation, .scroll])
|
2019-09-05 04:06:29 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-05 20:14:14 +02:00
|
|
|
|
|
|
|
|
|
func selectTodayFeed() {
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(SmartFeedsController.shared)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.todayFeed) {
|
|
|
|
|
self.selectFeed(SmartFeedsController.shared.todayFeed, animations: [.navigation, .scroll])
|
2019-09-05 20:14:14 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectAllUnreadFeed() {
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(SmartFeedsController.shared)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.unreadFeed) {
|
|
|
|
|
self.selectFeed(SmartFeedsController.shared.unreadFeed, animations: [.navigation, .scroll])
|
2019-09-05 20:14:14 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectStarredFeed() {
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(SmartFeedsController.shared)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.starredFeed) {
|
|
|
|
|
self.selectFeed(SmartFeedsController.shared.starredFeed, animations: [.navigation, .scroll])
|
2019-09-05 20:14:14 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-05 04:06:29 +02:00
|
|
|
|
|
2020-01-28 05:57:52 +01:00
|
|
|
|
func selectArticle(_ article: Article?, animations: Animations = []) {
|
2019-09-13 18:19:19 +02:00
|
|
|
|
guard article != currentArticle else { return }
|
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
currentArticle = article
|
2019-11-15 13:19:14 +01:00
|
|
|
|
activityManager.reading(feed: timelineFeed, article: article)
|
2019-08-24 21:57:51 +02:00
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
if article == nil {
|
2019-09-06 14:29:36 +02:00
|
|
|
|
if rootSplitViewController.isCollapsed {
|
2019-09-24 11:29:15 +02:00
|
|
|
|
if masterNavigationController.children.last is ArticleViewController {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
masterNavigationController.popViewController(animated: animations.contains(.navigation))
|
2019-09-06 14:29:36 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2020-01-01 00:55:39 +01:00
|
|
|
|
articleViewController?.article = nil
|
2019-08-01 13:53:34 +02:00
|
|
|
|
}
|
2020-01-28 05:57:52 +01:00
|
|
|
|
masterTimelineViewController?.updateArticleSelection(animations: animations)
|
2019-08-01 13:53:34 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 19:36:25 +02:00
|
|
|
|
let currentArticleViewController: ArticleViewController
|
2019-09-24 11:29:15 +02:00
|
|
|
|
if articleViewController == nil {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
currentArticleViewController = installArticleController(animated: animations.contains(.navigation))
|
2019-09-30 19:36:25 +02:00
|
|
|
|
} else {
|
|
|
|
|
currentArticleViewController = articleViewController!
|
2019-07-26 23:26:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-28 05:57:52 +01:00
|
|
|
|
masterTimelineViewController?.updateArticleSelection(animations: animations)
|
2020-01-01 00:55:39 +01:00
|
|
|
|
currentArticleViewController.article = article
|
2019-09-24 02:23:23 +02:00
|
|
|
|
|
2019-09-28 02:58:09 +02:00
|
|
|
|
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
|
2019-11-19 18:16:43 +01:00
|
|
|
|
savedSearchArticles = articles
|
|
|
|
|
savedSearchArticleIds = Set(articles.map { $0.articleID })
|
2019-11-24 17:27:02 +01:00
|
|
|
|
setTimelineFeed(nil, animated: true)
|
2019-11-19 18:16:43 +01:00
|
|
|
|
selectArticle(nil)
|
2019-09-01 21:49:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func endSearching() {
|
2019-11-15 13:19:14 +01:00
|
|
|
|
if let ip = currentFeedIndexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
|
2020-01-29 19:41:05 +01:00
|
|
|
|
emptyTheTimeline()
|
2019-11-15 13:19:14 +01:00
|
|
|
|
timelineFeed = feed
|
2019-12-09 02:14:33 +01:00
|
|
|
|
masterTimelineViewController?.reinitializeArticles(resetScroll: true)
|
2019-11-24 17:27:02 +01:00
|
|
|
|
replaceArticles(with: savedSearchArticles!, animated: true)
|
2019-09-03 19:25:27 +02:00
|
|
|
|
} else {
|
2019-11-24 17:27:02 +01:00
|
|
|
|
setTimelineFeed(nil, animated: true)
|
2019-08-31 22:53:47 +02:00
|
|
|
|
}
|
2019-09-12 17:32:58 +02:00
|
|
|
|
|
2019-11-19 18:16:43 +01:00
|
|
|
|
lastSearchString = ""
|
|
|
|
|
lastSearchScope = nil
|
|
|
|
|
savedSearchArticleIds = nil
|
|
|
|
|
savedSearchArticles = nil
|
|
|
|
|
isSearching = false
|
2019-09-12 17:32:58 +02:00
|
|
|
|
selectArticle(nil)
|
2019-12-09 02:31:55 +01:00
|
|
|
|
masterTimelineViewController?.focus()
|
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 {
|
2019-11-24 17:27:02 +01:00
|
|
|
|
setTimelineFeed(nil, animated: true)
|
2019-08-31 19:12:50 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-31 22:53:47 +02:00
|
|
|
|
if searchString != lastSearchString || searchScope != lastSearchScope {
|
|
|
|
|
|
|
|
|
|
switch searchScope {
|
|
|
|
|
case .global:
|
2019-11-24 17:27:02 +01:00
|
|
|
|
setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString)), animated: true)
|
2019-08-31 22:53:47 +02:00
|
|
|
|
case .timeline:
|
2019-11-24 17:27:02 +01:00
|
|
|
|
setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!)), animated: true)
|
2019-08-31 22:53:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-26 22:21:04 +01:00
|
|
|
|
func findPrevArticle(_ article: Article) -> Article? {
|
|
|
|
|
guard let index = articles.firstIndex(of: article), index > 0 else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return articles[index - 1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findNextArticle(_ article: Article) -> Article? {
|
|
|
|
|
guard let index = articles.firstIndex(of: article), index + 1 != articles.count else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return articles[index + 1]
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 18:32:19 +02:00
|
|
|
|
func selectPrevArticle() {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
if let article = prevArticle {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
selectArticle(article, animations: [.navigation, .scroll])
|
2019-07-06 18:32:19 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func selectNextArticle() {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
if let article = nextArticle {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
selectArticle(article, animations: [.navigation, .scroll])
|
2019-07-06 18:32:19 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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() {
|
|
|
|
|
return
|
|
|
|
|
}
|
2020-01-29 01:16:49 +01:00
|
|
|
|
|
|
|
|
|
if self.isSearching {
|
|
|
|
|
self.masterTimelineViewController?.hideSearch()
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 20:37:56 +01:00
|
|
|
|
selectNextUnreadFeed() {
|
2020-03-30 22:42:42 +02:00
|
|
|
|
self.selectNextUnreadArticleInTimeline()
|
2019-09-03 22:52:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-06 04:14:19 +02:00
|
|
|
|
func scrollOrGoToNextUnread() {
|
2019-09-24 11:29:15 +02:00
|
|
|
|
if articleViewController?.canScrollDown() ?? false {
|
|
|
|
|
articleViewController?.scrollPageDown()
|
2019-09-06 04:14:19 +02:00
|
|
|
|
} else {
|
|
|
|
|
selectNextUnread()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-20 00:26:09 +02:00
|
|
|
|
func markAllAsRead(_ articles: [Article]) {
|
2019-10-04 18:41:30 +02:00
|
|
|
|
markArticlesWithUndo(articles, statusKey: .read, flag: true)
|
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)
|
|
|
|
|
}
|
2020-01-03 08:16:55 +01:00
|
|
|
|
|
|
|
|
|
func canMarkAboveAsRead(for article: Article) -> Bool {
|
2020-01-07 21:54:21 +01:00
|
|
|
|
let articlesAboveArray = articles.articlesAbove(article: article)
|
|
|
|
|
return articlesAboveArray.canMarkAllAsRead()
|
2020-01-03 08:16:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-04 09:42:29 +01:00
|
|
|
|
func markAboveAsRead() {
|
|
|
|
|
guard let currentArticle = currentArticle else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
markAboveAsRead(currentArticle)
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 08:16:55 +01:00
|
|
|
|
func markAboveAsRead(_ article: Article) {
|
2020-01-07 21:54:21 +01:00
|
|
|
|
let articlesAboveArray = articles.articlesAbove(article: article)
|
|
|
|
|
markAllAsRead(articlesAboveArray)
|
2019-09-05 21:50:05 +02:00
|
|
|
|
}
|
2020-01-03 08:16:55 +01:00
|
|
|
|
|
|
|
|
|
func canMarkBelowAsRead(for article: Article) -> Bool {
|
2020-01-07 21:54:21 +01:00
|
|
|
|
let articleBelowArray = articles.articlesBelow(article: article)
|
|
|
|
|
return articleBelowArray.canMarkAllAsRead()
|
2020-01-03 08:16:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func markBelowAsRead() {
|
|
|
|
|
guard let currentArticle = currentArticle else {
|
2019-08-19 23:03:07 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-01-03 08:16:55 +01:00
|
|
|
|
|
|
|
|
|
markBelowAsRead(currentArticle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func markBelowAsRead(_ article: Article) {
|
2020-01-07 21:54:21 +01:00
|
|
|
|
let articleBelowArray = articles.articlesBelow(article: article)
|
|
|
|
|
markAllAsRead(articleBelowArray)
|
2019-08-19 23:03:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 21:50:05 +02:00
|
|
|
|
func markAsReadForCurrentArticle() {
|
|
|
|
|
if let article = currentArticle {
|
2019-10-04 18:41:30 +02:00
|
|
|
|
markArticlesWithUndo([article], statusKey: .read, flag: true)
|
2019-09-05 21:50:05 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func markAsUnreadForCurrentArticle() {
|
|
|
|
|
if let article = currentArticle {
|
2019-10-04 18:41:30 +02:00
|
|
|
|
markArticlesWithUndo([article], statusKey: .read, flag: false)
|
2019-09-05 21:50:05 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-06 18:49:53 +02:00
|
|
|
|
func toggleReadForCurrentArticle() {
|
|
|
|
|
if let article = currentArticle {
|
2019-10-04 18:41:30 +02:00
|
|
|
|
toggleRead(article)
|
2019-07-06 18:49:53 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
func toggleRead(_ article: Article) {
|
2020-02-18 22:49:29 +01:00
|
|
|
|
guard !article.status.read || article.isAvailableToMarkUnread else { return }
|
2019-10-04 18:41:30 +02:00
|
|
|
|
markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read)
|
2019-08-19 23:03:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 22:43:01 +02:00
|
|
|
|
func toggleStarredForCurrentArticle() {
|
2019-07-06 18:49:53 +02:00
|
|
|
|
if let article = currentArticle {
|
2019-10-04 18:41:30 +02:00
|
|
|
|
toggleStar(article)
|
2019-07-06 18:49:53 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
func toggleStar(_ article: Article) {
|
2019-10-04 18:41:30 +02:00
|
|
|
|
markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred)
|
2019-08-19 23:03:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-02-02 21:44:54 +01:00
|
|
|
|
func timelineFeedIsEqualTo(_ feed: WebFeed) -> Bool {
|
|
|
|
|
guard let timelineFeed = timelineFeed as? WebFeed else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return timelineFeed == feed
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func discloseWebFeed(_ webFeed: WebFeed, animations: Animations = [], completion: (() -> Void)? = nil) {
|
2020-01-16 01:53:12 +01:00
|
|
|
|
if isSearching {
|
|
|
|
|
masterTimelineViewController?.hideSearch()
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
guard let account = webFeed.account else {
|
2019-09-01 03:23:14 +02:00
|
|
|
|
completion?()
|
2020-02-18 02:40:40 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
|
|
|
|
|
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(account)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
if let parentFolder = parentFolder {
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(parentFolder)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let webFeedFeedID = webFeed.feedID {
|
|
|
|
|
self.treeControllerDelegate.addFilterException(webFeedFeedID)
|
|
|
|
|
}
|
|
|
|
|
if let parentFolderFeedID = parentFolder?.feedID {
|
|
|
|
|
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
2019-09-01 03:23:14 +02:00
|
|
|
|
}
|
2020-02-18 02:40:40 +01:00
|
|
|
|
|
|
|
|
|
rebuildBackingStores() {
|
|
|
|
|
self.treeControllerDelegate.resetFilterExceptions()
|
|
|
|
|
self.selectFeed(webFeed, animations: animations, completion: completion)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-19 22:45:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-19 02:12:24 +01:00
|
|
|
|
func showStatusBar() {
|
|
|
|
|
prefersStatusBarHidden = false
|
|
|
|
|
UIView.animate(withDuration: 0.15) {
|
|
|
|
|
self.rootSplitViewController.setNeedsStatusBarAppearanceUpdate()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func hideStatusBar() {
|
|
|
|
|
prefersStatusBarHidden = true
|
|
|
|
|
UIView.animate(withDuration: 0.15) {
|
|
|
|
|
self.rootSplitViewController.setNeedsStatusBarAppearanceUpdate()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-11 19:30:16 +01:00
|
|
|
|
func showSettings(scrollToArticlesSection: Bool = false) {
|
2019-10-21 18:51:33 +02:00
|
|
|
|
let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController
|
2019-10-22 01:02:44 +02:00
|
|
|
|
let settingsViewController = settingsNavController.topViewController as! SettingsViewController
|
2020-01-11 19:30:16 +01:00
|
|
|
|
settingsViewController.scrollToArticlesSection = scrollToArticlesSection
|
2019-10-21 18:51:33 +02:00
|
|
|
|
settingsNavController.modalPresentationStyle = .formSheet
|
2019-10-22 01:02:44 +02:00
|
|
|
|
settingsViewController.presentingParentController = rootSplitViewController
|
|
|
|
|
rootSplitViewController.present(settingsNavController, animated: true)
|
2019-07-06 19:25:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-24 02:58:18 +02:00
|
|
|
|
func showAccountInspector(for account: Account) {
|
|
|
|
|
let accountInspectorNavController =
|
|
|
|
|
UIStoryboard.inspector.instantiateViewController(identifier: "AccountInspectorNavigationViewController") as! UINavigationController
|
|
|
|
|
let accountInspectorController = accountInspectorNavController.topViewController as! AccountInspectorViewController
|
|
|
|
|
accountInspectorNavController.modalPresentationStyle = .formSheet
|
|
|
|
|
accountInspectorNavController.preferredContentSize = AccountInspectorViewController.preferredContentSizeForFormSheetDisplay
|
|
|
|
|
accountInspectorController.isModal = true
|
|
|
|
|
accountInspectorController.account = account
|
|
|
|
|
rootSplitViewController.present(accountInspectorNavController, animated: true)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-28 02:45:09 +02:00
|
|
|
|
func showFeedInspector() {
|
2019-11-15 13:19:14 +01:00
|
|
|
|
guard let feed = timelineFeed as? WebFeed else {
|
2019-09-28 02:45:09 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-07 13:40:10 +01:00
|
|
|
|
showFeedInspector(for: feed)
|
2019-09-28 02:45:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
func showFeedInspector(for feed: WebFeed) {
|
2019-11-07 13:40:10 +01:00
|
|
|
|
let feedInspectorNavController =
|
|
|
|
|
UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController
|
2019-11-15 03:11:41 +01:00
|
|
|
|
let feedInspectorController = feedInspectorNavController.topViewController as! WebFeedInspectorViewController
|
2019-11-14 00:02:14 +01:00
|
|
|
|
feedInspectorNavController.modalPresentationStyle = .formSheet
|
2019-11-15 03:11:41 +01:00
|
|
|
|
feedInspectorNavController.preferredContentSize = WebFeedInspectorViewController.preferredContentSizeForFormSheetDisplay
|
|
|
|
|
feedInspectorController.webFeed = feed
|
2019-11-07 13:40:10 +01:00
|
|
|
|
rootSplitViewController.present(feedInspectorNavController, animated: true)
|
2019-09-28 14:00:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-15 19:03:28 +02:00
|
|
|
|
func showAdd(_ type: AddControllerType, initialFeed: String? = nil, initialFeedName: String? = nil) {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
selectFeed(nil)
|
2019-09-06 17:57:37 +02:00
|
|
|
|
|
2019-09-05 22:07:35 +02:00
|
|
|
|
let addViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController
|
2019-09-15 19:03:28 +02:00
|
|
|
|
|
2019-09-05 22:07:35 +02:00
|
|
|
|
let containerController = addViewController.topViewController as! AddContainerViewController
|
|
|
|
|
containerController.initialControllerType = type
|
2019-09-15 19:03:28 +02:00
|
|
|
|
containerController.initialFeed = initialFeed
|
|
|
|
|
containerController.initialFeedName = initialFeedName
|
|
|
|
|
|
2019-07-06 19:25:45 +02:00
|
|
|
|
addViewController.modalPresentationStyle = .formSheet
|
|
|
|
|
addViewController.preferredContentSize = AddContainerViewController.preferredContentSizeForFormSheetDisplay
|
|
|
|
|
masterFeedViewController.present(addViewController, animated: true)
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-01 02:06:27 +01:00
|
|
|
|
func showFullScreenImage(image: UIImage, imageTitle: String?, transitioningDelegate: UIViewControllerTransitioningDelegate) {
|
2019-10-17 03:20:36 +02:00
|
|
|
|
let imageVC = UIStoryboard.main.instantiateController(ofType: ImageViewController.self)
|
|
|
|
|
imageVC.image = image
|
2020-01-01 02:06:27 +01:00
|
|
|
|
imageVC.imageTitle = imageTitle
|
2019-10-17 03:20:36 +02:00
|
|
|
|
imageVC.modalPresentationStyle = .currentContext
|
|
|
|
|
imageVC.transitioningDelegate = transitioningDelegate
|
|
|
|
|
rootSplitViewController.present(imageVC, animated: true)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-04 23:24:16 +02:00
|
|
|
|
func homePageURLForFeed(_ indexPath: IndexPath) -> URL? {
|
|
|
|
|
guard let node = nodeFor(indexPath),
|
2019-11-15 03:11:41 +01:00
|
|
|
|
let feed = node.representedObject as? WebFeed,
|
2019-09-04 23:24:16 +02:00
|
|
|
|
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: [:])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 16:11:33 +02:00
|
|
|
|
func showBrowserForArticle(_ article: Article) {
|
|
|
|
|
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
|
2019-08-20 00:38:30 +02:00
|
|
|
|
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() {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
if currentArticle == nil && articles.count > 0 {
|
|
|
|
|
selectArticle(articles[0])
|
2019-09-05 04:06:29 +02:00
|
|
|
|
}
|
2019-09-05 17:46:24 +02:00
|
|
|
|
masterTimelineViewController?.focus()
|
2019-09-05 04:06:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func navigateToDetail() {
|
2019-09-24 11:29:15 +02:00
|
|
|
|
articleViewController?.focus()
|
2019-09-05 04:06:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2020-03-13 22:03:42 +01:00
|
|
|
|
masterTimelineViewController?.updateUI()
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
guard !isThreePanelMode else {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let articleViewController = (secondaryViewController as? UINavigationController)?.topViewController as? ArticleViewController {
|
2019-12-01 19:04:28 +01:00
|
|
|
|
if currentArticle != nil {
|
|
|
|
|
masterNavigationController.pushViewController(articleViewController, animated: false)
|
|
|
|
|
}
|
2019-11-20 23:41:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-01 19:04:28 +01:00
|
|
|
|
return true
|
2019-08-02 23:46:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-04 01:10:49 +01:00
|
|
|
|
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
|
2020-03-13 22:03:42 +01:00
|
|
|
|
masterTimelineViewController?.updateUI()
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
guard !isThreePanelMode else {
|
|
|
|
|
return subSplitViewController
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-01 20:45:18 +01:00
|
|
|
|
if let articleViewController = masterNavigationController.viewControllers.last as? ArticleViewController {
|
2020-01-10 05:22:29 +01:00
|
|
|
|
articleViewController.showBars(self)
|
2019-12-01 20:45:18 +01:00
|
|
|
|
masterNavigationController.popViewController(animated: false)
|
2019-11-20 23:41:13 +01:00
|
|
|
|
let controller = addNavControllerIfNecessary(articleViewController, showButton: true)
|
|
|
|
|
return controller
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-01 20:45:18 +01:00
|
|
|
|
if currentArticle == nil {
|
|
|
|
|
let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
|
|
|
|
|
articleViewController.coordinator = self
|
2019-11-20 23:41:13 +01:00
|
|
|
|
let controller = addNavControllerIfNecessary(articleViewController, showButton: true)
|
|
|
|
|
return controller
|
2019-11-04 01:10:49 +01:00
|
|
|
|
}
|
2019-11-20 23:41:13 +01:00
|
|
|
|
|
2019-11-05 22:46:28 +01:00
|
|
|
|
return nil
|
2019-11-04 01:10:49 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 22:31:11 +02:00
|
|
|
|
// MARK: UINavigationControllerDelegate
|
|
|
|
|
|
|
|
|
|
extension SceneCoordinator: UINavigationControllerDelegate {
|
2019-11-19 02:12:24 +01:00
|
|
|
|
|
2019-09-01 22:31:11 +02:00
|
|
|
|
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
|
2019-09-11 00:02:41 +02:00
|
|
|
|
|
2019-10-01 11:31:42 +02:00
|
|
|
|
if UIApplication.shared.applicationState == .background {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-19 02:12:24 +01:00
|
|
|
|
|
2019-09-11 00:18:10 +02:00
|
|
|
|
// If we are showing the Feeds and only the feeds start clearing stuff
|
2019-11-01 02:55:08 +01:00
|
|
|
|
if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending {
|
2019-09-01 22:31:11 +02:00
|
|
|
|
activityManager.invalidateCurrentActivities()
|
2020-01-28 05:57:52 +01:00
|
|
|
|
selectFeed(nil, animations: [.scroll, .select, .navigation])
|
2019-10-01 10:51:48 +02:00
|
|
|
|
return
|
2019-09-01 22:31:11 +02:00
|
|
|
|
}
|
2019-11-01 02:55:08 +01:00
|
|
|
|
|
2019-10-01 10:51:48 +02:00
|
|
|
|
// If we are using a phone and navigate away from the detail, clear up the article resources (including activity).
|
|
|
|
|
// Don't clear it if we have pushed an ArticleViewController, but don't yet see it on the navigation stack.
|
|
|
|
|
// This happens when we are going to the next unread and we need to grab another timeline to continue. The
|
|
|
|
|
// ArticleViewController will be pushed, but we will breifly show the Timeline. Don't clear things out when that happens.
|
|
|
|
|
if viewController === masterTimelineViewController && !isThreePanelMode && rootSplitViewController.isCollapsed && !isArticleViewControllerPending {
|
2019-09-30 19:36:25 +02:00
|
|
|
|
currentArticle = nil
|
2020-01-28 05:57:52 +01:00
|
|
|
|
masterTimelineViewController?.updateArticleSelection(animations: [.scroll, .select, .navigation])
|
2019-09-30 19:36:25 +02:00
|
|
|
|
activityManager.invalidateReading()
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
|
|
|
|
// Restore any bars hidden by the article controller
|
|
|
|
|
showStatusBar()
|
|
|
|
|
navigationController.setNavigationBarHidden(false, animated: true)
|
|
|
|
|
navigationController.setToolbarHidden(false, animated: true)
|
2019-10-01 10:51:48 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 22:31:11 +02:00
|
|
|
|
}
|
2019-09-11 00:18:10 +02:00
|
|
|
|
|
2019-09-01 22:31:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
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-10-04 18:41:30 +02:00
|
|
|
|
func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) {
|
|
|
|
|
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
runCommand(markReadCommand)
|
|
|
|
|
}
|
|
|
|
|
|
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-11-24 17:27:02 +01:00
|
|
|
|
|
2020-01-27 20:58:32 +01:00
|
|
|
|
func rebuildArticleDictionaries() {
|
|
|
|
|
var idDictionary = [String: Article]()
|
|
|
|
|
|
|
|
|
|
articles.forEach { article in
|
|
|
|
|
idDictionary[article.articleID] = article
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_idToArticleDictionary = idDictionary
|
|
|
|
|
articleDictionaryNeedsUpdate = false
|
|
|
|
|
}
|
2020-02-18 02:40:40 +01:00
|
|
|
|
|
2020-02-18 18:30:58 +01:00
|
|
|
|
func ensureFeedIsAvailableToSelect(_ feed: Feed, completion: @escaping () -> Void) {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
addToFilterExeptionsIfNecessary(feed)
|
|
|
|
|
addShadowTableToFilterExceptions()
|
|
|
|
|
|
|
|
|
|
rebuildBackingStores() {
|
|
|
|
|
self.treeControllerDelegate.resetFilterExceptions()
|
|
|
|
|
completion()
|
2020-01-29 00:51:50 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-18 02:40:40 +01:00
|
|
|
|
|
|
|
|
|
func addToFilterExeptionsIfNecessary(_ feed: Feed?) {
|
|
|
|
|
if isReadFeedsFiltered, let feedID = feed?.feedID {
|
|
|
|
|
if feed is SmartFeed {
|
2020-01-29 00:51:50 +01:00
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
} else if let folderFeed = feed as? Folder {
|
2020-01-29 00:51:50 +01:00
|
|
|
|
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
|
|
|
|
}
|
2020-02-18 02:40:40 +01:00
|
|
|
|
} else if let webFeed = feed as? WebFeed {
|
2020-01-29 00:51:50 +01:00
|
|
|
|
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
2020-01-29 19:18:17 +01:00
|
|
|
|
addParentFolderToFilterExceptions(webFeed)
|
2020-01-29 00:51:50 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-29 19:18:17 +01:00
|
|
|
|
func addParentFolderToFilterExceptions(_ feed: Feed) {
|
|
|
|
|
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
|
|
|
|
let folder = node.parent?.representedObject as? Folder,
|
|
|
|
|
let folderFeedID = folder.feedID else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
treeControllerDelegate.addFilterException(folderFeedID)
|
|
|
|
|
}
|
2020-02-18 02:40:40 +01:00
|
|
|
|
|
|
|
|
|
func addShadowTableToFilterExceptions() {
|
|
|
|
|
for section in shadowTable {
|
|
|
|
|
for node in section {
|
|
|
|
|
if let feed = node.representedObject as? Feed, let feedID = feed.feedID {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-28 07:43:54 +01:00
|
|
|
|
|
|
|
|
|
func queueRebuildBackingStores() {
|
|
|
|
|
rebuildBackingStoresQueue.add(self, #selector(rebuildBackingStoresWithDefaults))
|
|
|
|
|
}
|
2020-01-29 19:18:17 +01:00
|
|
|
|
|
2020-03-28 00:24:57 +01:00
|
|
|
|
@objc func rebuildBackingStoresWithDefaults() {
|
|
|
|
|
rebuildBackingStores()
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil, completion: (() -> Void)? = nil) {
|
|
|
|
|
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
|
|
|
|
|
|
|
|
|
addToFilterExeptionsIfNecessary(timelineFeed)
|
|
|
|
|
treeController.rebuild()
|
|
|
|
|
treeControllerDelegate.resetFilterExceptions()
|
|
|
|
|
|
|
|
|
|
updateExpandedNodes?()
|
|
|
|
|
rebuildShadowTable()
|
|
|
|
|
masterFeedViewController.reloadFeeds(initialLoad: initialLoad, completion: completion)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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)!
|
|
|
|
|
|
2019-11-25 01:29:00 +01:00
|
|
|
|
if isExpanded(sectionNode) {
|
2019-09-08 15:55:07 +02:00
|
|
|
|
for node in sectionNode.childNodes {
|
2019-08-31 18:38:03 +02:00
|
|
|
|
result.append(node)
|
2019-11-25 01:29:00 +01:00
|
|
|
|
if isExpanded(node) {
|
2019-08-31 18:38:03 +02:00
|
|
|
|
for child in node.childNodes {
|
|
|
|
|
result.append(child)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shadowTable.append(result)
|
|
|
|
|
|
|
|
|
|
}
|
2020-01-30 01:06:35 +01:00
|
|
|
|
|
|
|
|
|
// If we have a current Feed IndexPath it is no longer valid and needs reset.
|
|
|
|
|
if currentFeedIndexPath != nil {
|
|
|
|
|
currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject)
|
|
|
|
|
}
|
2019-08-31 18:38:03 +02:00
|
|
|
|
}
|
2020-01-20 00:44:13 +01:00
|
|
|
|
|
|
|
|
|
func shadowTableContains(_ feed: Feed) -> Bool {
|
|
|
|
|
for section in shadowTable {
|
|
|
|
|
for node in section {
|
|
|
|
|
if let nodeFeed = node.representedObject as? Feed, nodeFeed.feedID == feed.feedID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2020-01-27 05:18:43 +01:00
|
|
|
|
|
|
|
|
|
func clearTimelineIfNoLongerAvailable() {
|
|
|
|
|
if let feed = timelineFeed, !shadowTableContains(feed) {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
selectFeed(nil, deselectArticle: true)
|
2020-01-27 05:18:43 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-31 18:38:03 +02:00
|
|
|
|
|
2019-09-11 21:00:07 +02:00
|
|
|
|
func nodeFor(_ indexPath: IndexPath) -> Node? {
|
|
|
|
|
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return shadowTable[indexPath.section][indexPath.row]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func indexPathFor(_ object: AnyObject) -> IndexPath? {
|
|
|
|
|
guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return indexPathFor(node)
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 17:27:02 +01:00
|
|
|
|
func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
timelineFeed = feed
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-11-24 17:27:02 +01:00
|
|
|
|
fetchAndReplaceArticlesAsync(animated: animated) {
|
2019-12-09 02:14:33 +01:00
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles(resetScroll: true)
|
2019-11-19 18:16:43 +01:00
|
|
|
|
completion?()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateShowNamesAndIcons() {
|
|
|
|
|
|
|
|
|
|
if timelineFeed is WebFeed {
|
2020-04-18 14:53:56 +02:00
|
|
|
|
showFeedNames = {
|
|
|
|
|
for article in articles {
|
2020-04-20 00:29:11 +02:00
|
|
|
|
if !article.byline().isEmpty {
|
2020-04-18 14:53:56 +02:00
|
|
|
|
return .byline
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return .none
|
|
|
|
|
}()
|
2019-11-19 18:16:43 +01:00
|
|
|
|
} else {
|
2020-04-18 14:53:56 +02:00
|
|
|
|
showFeedNames = .feed
|
2019-11-19 18:16:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-18 14:53:56 +02:00
|
|
|
|
if showFeedNames == .feed {
|
2019-11-06 01:05:57 +01:00
|
|
|
|
self.showIcons = true
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-20 00:29:11 +02:00
|
|
|
|
if showFeedNames == .none {
|
|
|
|
|
self.showIcons = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
for article in articles {
|
|
|
|
|
if let authors = article.authors {
|
|
|
|
|
for author in authors {
|
|
|
|
|
if author.avatarURL != nil {
|
2019-11-06 01:05:57 +01:00
|
|
|
|
self.showIcons = true
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-06 01:05:57 +01:00
|
|
|
|
self.showIcons = false
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func markExpanded(_ containerIdentifiable: ContainerIdentifiable) {
|
|
|
|
|
if let containerID = containerIdentifiable.containerID {
|
2019-11-27 00:32:30 +01:00
|
|
|
|
expandedTable.insert(containerID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func markExpanded(_ node: Node) {
|
|
|
|
|
if let containerIdentifiable = node.representedObject as? ContainerIdentifiable {
|
|
|
|
|
markExpanded(containerIdentifiable)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func unmarkExpanded(_ containerIdentifiable: ContainerIdentifiable) {
|
|
|
|
|
if let containerID = containerIdentifiable.containerID {
|
2019-11-27 00:32:30 +01:00
|
|
|
|
expandedTable.remove(containerID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func unmarkExpanded(_ node: Node) {
|
|
|
|
|
if let containerIdentifiable = node.representedObject as? ContainerIdentifiable {
|
|
|
|
|
unmarkExpanded(containerIdentifiable)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 = {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
if let articleRow = currentArticleRow {
|
|
|
|
|
return articleRow
|
2019-09-06 01:02:40 +02:00
|
|
|
|
} 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 {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
selectArticle(article)
|
2019-09-06 01:02:40 +02:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 01:29:00 +01:00
|
|
|
|
if isExpanded(node) {
|
2019-09-06 01:02:40 +02:00
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if unreadCountProvider.unreadCount > 0 {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
selectFeed(indexPath: prevIndexPath, animations: [.scroll, .navigation])
|
2019-09-06 01:02:40 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Select Next Unread
|
|
|
|
|
|
2019-08-04 00:07:43 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
func selectFirstUnreadArticleInTimeline() -> Bool {
|
2019-10-10 04:39:11 +02:00
|
|
|
|
return selectNextArticleInTimeline(startingRow: 0, animated: true)
|
2019-08-04 00:07:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
func selectNextUnreadArticleInTimeline() -> Bool {
|
|
|
|
|
let startingRow: Int = {
|
2019-09-11 16:11:33 +02:00
|
|
|
|
if let articleRow = currentArticleRow {
|
|
|
|
|
return articleRow + 1
|
2019-06-29 20:35:12 +02:00
|
|
|
|
} else {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2019-10-10 04:39:11 +02:00
|
|
|
|
return selectNextArticleInTimeline(startingRow: startingRow, animated: false)
|
2019-08-04 00:07:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-10 04:39:11 +02:00
|
|
|
|
func selectNextArticleInTimeline(startingRow: Int, animated: Bool) -> 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 {
|
2020-01-28 05:57:52 +01:00
|
|
|
|
selectArticle(article, animations: [.scroll, .navigation])
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 20:37:56 +01:00
|
|
|
|
func selectNextUnreadFeed(completion: @escaping () -> Void) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2019-11-24 20:37:56 +01:00
|
|
|
|
selectNextUnreadFeed(startingWith: nextIndexPath) { found in
|
|
|
|
|
if !found {
|
|
|
|
|
self.selectNextUnreadFeed(startingWith: IndexPath(row: 0, section: 0)) { _ in
|
|
|
|
|
completion()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
completion()
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 20:37:56 +01:00
|
|
|
|
func selectNextUnreadFeed(startingWith indexPath: IndexPath, completion: @escaping (Bool) -> Void) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}()
|
2020-04-13 03:23:20 +02:00
|
|
|
|
|
|
|
|
|
for j in startingRow..<shadowTable[i].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()
|
2019-11-24 21:36:17 +01:00
|
|
|
|
completion(false)
|
2019-11-24 20:37:56 +01:00
|
|
|
|
return
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
2020-04-13 03:23:20 +02:00
|
|
|
|
|
2019-11-25 01:29:00 +01:00
|
|
|
|
if isExpanded(node) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if unreadCountProvider.unreadCount > 0 {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
selectFeed(indexPath: nextIndexPath, animations: [.scroll, .navigation], deselectArticle: false) {
|
2019-11-24 21:36:17 +01:00
|
|
|
|
self.currentArticle = nil
|
2019-11-24 20:37:56 +01:00
|
|
|
|
completion(true)
|
|
|
|
|
}
|
|
|
|
|
return
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 20:37:56 +01:00
|
|
|
|
completion(false)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Fetching Articles
|
|
|
|
|
|
|
|
|
|
func emptyTheTimeline() {
|
|
|
|
|
if !articles.isEmpty {
|
2019-11-24 17:27:02 +01:00
|
|
|
|
replaceArticles(with: Set<Article>(), animated: false)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-13 15:29:56 +02:00
|
|
|
|
func sortParametersDidChange() {
|
2019-11-24 17:27:02 +01:00
|
|
|
|
replaceArticles(with: Set(articles), animated: true)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
2019-09-13 15:29:56 +02:00
|
|
|
|
|
2019-11-24 17:27:02 +01:00
|
|
|
|
func replaceArticles(with unsortedArticles: Set<Article>, animated: Bool) {
|
2019-09-09 00:41:00 +02:00
|
|
|
|
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
|
2019-11-24 17:27:02 +01:00
|
|
|
|
replaceArticles(with: sortedArticles, animated: animated)
|
2019-11-19 18:16:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 17:27:02 +01:00
|
|
|
|
func replaceArticles(with sortedArticles: ArticleArray, animated: Bool) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
if articles != sortedArticles {
|
|
|
|
|
articles = sortedArticles
|
2019-11-19 18:16:43 +01:00
|
|
|
|
updateShowNamesAndIcons()
|
2019-09-03 23:07:02 +02:00
|
|
|
|
updateUnreadCount()
|
2019-11-24 17:27:02 +01:00
|
|
|
|
masterTimelineViewController?.reloadArticles(animated: animated)
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func queueFetchAndMergeArticles() {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticlesAsync))
|
2020-01-24 02:07:20 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func fetchAndMergeArticlesAsync() {
|
2020-02-23 19:57:20 +01:00
|
|
|
|
fetchAndMergeArticlesAsync(animated: true) {
|
|
|
|
|
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
|
2020-02-24 02:12:02 +01:00
|
|
|
|
self.masterTimelineViewController?.restoreSelectionIfNecessary(adjustScroll: false)
|
2020-02-23 19:57:20 +01:00
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 02:07:20 +01:00
|
|
|
|
func fetchAndMergeArticlesAsync(animated: Bool = true, completion: (() -> Void)? = nil) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
|
guard let timelineFeed = timelineFeed else {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
|
fetchUnsortedArticlesAsync(for: [timelineFeed]) { [weak self] (unsortedArticles) in
|
2019-08-21 22:27:53 +02:00
|
|
|
|
// 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)
|
|
|
|
|
}
|
2020-01-24 02:07:20 +01:00
|
|
|
|
if article.account?.existingWebFeed(withWebFeedID: article.webFeedID) == nil {
|
|
|
|
|
updatedArticles.remove(article)
|
|
|
|
|
}
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
2019-08-21 22:27:53 +02:00
|
|
|
|
|
2020-01-24 02:07:20 +01:00
|
|
|
|
strongSelf.replaceArticles(with: updatedArticles, animated: animated)
|
|
|
|
|
completion?()
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 22:58:55 +02:00
|
|
|
|
func cancelPendingAsyncFetches() {
|
|
|
|
|
fetchSerialNumber += 1
|
|
|
|
|
fetchRequestQueue.cancelAllRequests()
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 17:27:02 +01:00
|
|
|
|
func fetchAndReplaceArticlesAsync(animated: Bool, 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()
|
2019-11-29 21:31:15 +01:00
|
|
|
|
guard let timelineFeed = timelineFeed else {
|
2019-08-21 22:58:55 +02:00
|
|
|
|
emptyTheTimeline()
|
2019-11-19 18:16:43 +01:00
|
|
|
|
completion()
|
2019-08-21 22:58:55 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
2019-11-29 21:31:15 +01:00
|
|
|
|
var fetchers = [ArticleFetcher]()
|
|
|
|
|
fetchers.append(timelineFeed)
|
|
|
|
|
if exceptionArticleFetcher != nil {
|
|
|
|
|
fetchers.append(exceptionArticleFetcher!)
|
|
|
|
|
exceptionArticleFetcher = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetchUnsortedArticlesAsync(for: fetchers) { [weak self] (articles) in
|
2019-11-24 17:27:02 +01:00
|
|
|
|
self?.replaceArticles(with: articles, animated: animated)
|
2019-08-29 22:02:45 +02:00
|
|
|
|
completion()
|
2019-08-21 22:58:55 +02:00
|
|
|
|
}
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
2019-08-21 22:27:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-15 02:01:34 +01:00
|
|
|
|
func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) {
|
2019-08-21 22:27:53 +02:00
|
|
|
|
// 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()
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
2019-11-27 18:43:36 +01:00
|
|
|
|
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadArticlesFiltered, representedObjects: representedObjects) { [weak self] (articles, operation) in
|
2019-08-21 22:27:53 +02:00
|
|
|
|
precondition(Thread.isMainThread)
|
|
|
|
|
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-12-15 02:01:34 +01:00
|
|
|
|
completion(articles)
|
2019-08-21 22:27:53 +02:00
|
|
|
|
}
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
2019-08-21 22:27:53 +02:00
|
|
|
|
fetchRequestQueue.add(fetchOperation)
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
func timelineFetcherContainsAnyPseudoFeed() -> Bool {
|
2019-11-15 13:19:14 +01:00
|
|
|
|
if timelineFeed is PseudoFeed {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-29 22:02:45 +02:00
|
|
|
|
func timelineFetcherContainsAnyFolder() -> Bool {
|
2019-11-15 13:19:14 +01:00
|
|
|
|
if timelineFeed is Folder {
|
2019-08-29 22:02:45 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
func timelineFetcherContainsAnyFeed(_ feeds: Set<WebFeed>) -> Bool {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
|
|
|
|
|
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
|
if let feed = timelineFeed as? WebFeed {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
for oneFeed in feeds {
|
2019-11-15 03:11:41 +01:00
|
|
|
|
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-11-15 13:19:14 +01:00
|
|
|
|
} else if let folder = timelineFeed as? Folder {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
for oneFeed in feeds {
|
2019-11-15 03:11:41 +01:00
|
|
|
|
if folder.hasWebFeed(with: oneFeed.webFeedID) || folder.hasWebFeed(withURL: oneFeed.url) {
|
2019-06-29 20:35:12 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 00:32:30 +01:00
|
|
|
|
// MARK: Three Panel Mode
|
2019-07-19 22:59:46 +02:00
|
|
|
|
|
2019-09-07 16:01:29 +02:00
|
|
|
|
func installTimelineControllerIfNecessary(animated: Bool) {
|
|
|
|
|
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
|
2019-11-13 22:22:22 +01:00
|
|
|
|
isTimelineViewControllerPending = true
|
2019-09-07 16:01:29 +02:00
|
|
|
|
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
|
|
|
|
|
masterTimelineViewController!.coordinator = self
|
|
|
|
|
navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: animated)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
@discardableResult
|
2020-01-29 19:30:52 +01:00
|
|
|
|
func installArticleController(state: ArticleViewController.State? = nil, animated: Bool) -> ArticleViewController {
|
2019-09-10 01:07:49 +02:00
|
|
|
|
|
2019-10-01 10:51:48 +02:00
|
|
|
|
isArticleViewControllerPending = true
|
|
|
|
|
|
2020-01-21 19:05:47 +01:00
|
|
|
|
let articleController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
|
|
|
|
|
articleController.coordinator = self
|
|
|
|
|
articleController.article = currentArticle
|
2020-01-29 19:30:52 +01:00
|
|
|
|
articleController.restoreState = state
|
2019-11-20 23:41:13 +01:00
|
|
|
|
|
2019-09-10 14:22:32 +02:00
|
|
|
|
if let subSplit = subSplitViewController {
|
2019-09-24 11:29:15 +02:00
|
|
|
|
let controller = addNavControllerIfNecessary(articleController, showButton: false)
|
2019-09-10 14:22:32 +02:00
|
|
|
|
subSplit.showDetailViewController(controller, sender: self)
|
2019-11-20 23:41:13 +01:00
|
|
|
|
} else if rootSplitViewController.isCollapsed || wasRootSplitViewControllerCollapsed {
|
|
|
|
|
masterNavigationController.pushViewController(articleController, animated: animated)
|
2019-08-02 02:59:56 +02:00
|
|
|
|
} else {
|
2019-09-24 11:29:15 +02:00
|
|
|
|
let controller = addNavControllerIfNecessary(articleController, showButton: true)
|
2019-09-11 01:26:27 +02:00
|
|
|
|
rootSplitViewController.showDetailViewController(controller, sender: self)
|
2019-09-10 01:07:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
return articleController
|
|
|
|
|
|
2019-08-02 02:59:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addNavControllerIfNecessary(_ controller: UIViewController, showButton: Bool) -> UIViewController {
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
// You will sometimes get a compact horizontal size class while in three panel mode. Dunno why it lies.
|
|
|
|
|
if rootSplitViewController.traitCollection.horizontalSizeClass == .compact && !isThreePanelMode {
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
2019-07-26 23:26:22 +02:00
|
|
|
|
return controller
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
2019-07-26 23:26:22 +02:00
|
|
|
|
} else {
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
2019-11-19 02:12:24 +01:00
|
|
|
|
let navController = InteractiveNavigationController.template(rootViewController: controller)
|
2019-07-26 23:26:22 +02:00
|
|
|
|
navController.isToolbarHidden = false
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
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
|
|
|
|
}
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
2019-07-26 23:26:22 +02:00
|
|
|
|
return navController
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
2019-07-26 23:26:22 +02:00
|
|
|
|
}
|
2019-09-11 01:26:27 +02:00
|
|
|
|
|
2019-07-26 23:26:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
func installSubSplit() {
|
2019-08-02 16:12:15 +02:00
|
|
|
|
rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.30
|
2019-07-26 23:26:22 +02:00
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
subSplitViewController = UISplitViewController()
|
|
|
|
|
subSplitViewController!.preferredDisplayMode = .allVisible
|
|
|
|
|
subSplitViewController!.viewControllers = [InteractiveNavigationController.template()]
|
|
|
|
|
subSplitViewController!.preferredPrimaryColumnWidthFraction = 0.4285
|
2019-08-02 02:59:56 +02:00
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
rootSplitViewController.showDetailViewController(subSplitViewController!, sender: self)
|
|
|
|
|
rootSplitViewController.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .regular), forChild: subSplitViewController!)
|
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-11-20 23:41:13 +01:00
|
|
|
|
func configureThreePanelMode() {
|
2020-01-29 19:30:52 +01:00
|
|
|
|
articleViewController?.stopArticleExtractorIfProcessing()
|
|
|
|
|
let articleViewControllerState = articleViewController?.currentState
|
2019-08-01 23:14:34 +02:00
|
|
|
|
defer {
|
|
|
|
|
masterNavigationController.viewControllers = [masterFeedViewController]
|
|
|
|
|
}
|
2019-09-09 23:59:24 +02:00
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
if rootSplitViewController.viewControllers.last is InteractiveNavigationController {
|
|
|
|
|
_ = rootSplitViewController.viewControllers.popLast()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
installSubSplit()
|
2019-09-10 14:49:24 +02:00
|
|
|
|
installTimelineControllerIfNecessary(animated: false)
|
2019-09-12 17:27:03 +02:00
|
|
|
|
masterTimelineViewController?.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
|
|
|
|
|
masterTimelineViewController?.navigationItem.leftItemsSupplementBackButton = true
|
|
|
|
|
|
2020-01-29 19:30:52 +01:00
|
|
|
|
installArticleController(state: articleViewControllerState, animated: false)
|
2019-09-09 23:59:24 +02:00
|
|
|
|
|
2019-09-10 23:38:59 +02:00
|
|
|
|
masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true)
|
2019-11-11 23:59:42 +01:00
|
|
|
|
masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false)
|
2019-08-01 23:14:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
func configureStandardPanelMode() {
|
2020-01-29 19:30:52 +01:00
|
|
|
|
articleViewController?.stopArticleExtractorIfProcessing()
|
|
|
|
|
let articleViewControllerState = articleViewController?.currentState
|
2019-08-01 23:14:34 +02:00
|
|
|
|
rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension
|
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
// Set the is Pending flags early to prevent the navigation controller delegate from thinking that we
|
|
|
|
|
// swiping around in the user interface
|
|
|
|
|
isTimelineViewControllerPending = true
|
|
|
|
|
isArticleViewControllerPending = true
|
2019-08-01 23:14:34 +02:00
|
|
|
|
|
2019-11-20 23:41:13 +01:00
|
|
|
|
masterNavigationController.viewControllers = [masterFeedViewController]
|
|
|
|
|
if rootSplitViewController.viewControllers.last is UISplitViewController {
|
|
|
|
|
subSplitViewController = nil
|
|
|
|
|
_ = rootSplitViewController.viewControllers.popLast()
|
2019-08-01 23:14:34 +02:00
|
|
|
|
}
|
2019-11-20 23:41:13 +01:00
|
|
|
|
|
|
|
|
|
if currentFeedIndexPath != nil {
|
|
|
|
|
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
|
|
|
|
|
masterTimelineViewController!.coordinator = self
|
|
|
|
|
masterNavigationController.pushViewController(masterTimelineViewController!, animated: false)
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-29 19:30:52 +01:00
|
|
|
|
installArticleController(state: articleViewControllerState, animated: false)
|
2019-08-01 23:14:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-24 21:57:51 +02:00
|
|
|
|
// MARK: NSUserActivity
|
|
|
|
|
|
2019-11-26 23:33:11 +01:00
|
|
|
|
func windowState() -> [AnyHashable: Any] {
|
2019-11-27 18:43:36 +01:00
|
|
|
|
let containerExpandedWindowState = expandedTable.map( { $0.userInfo })
|
|
|
|
|
var readArticlesFilterState = [[AnyHashable: AnyHashable]: Bool]()
|
|
|
|
|
for key in readFilterEnabledTable.keys {
|
|
|
|
|
readArticlesFilterState[key.userInfo] = readFilterEnabledTable[key]
|
|
|
|
|
}
|
2019-11-26 23:33:11 +01:00
|
|
|
|
return [
|
2019-11-27 03:23:12 +01:00
|
|
|
|
UserInfoKey.readFeedsFilterState: isReadFeedsFiltered,
|
2019-11-27 18:43:36 +01:00
|
|
|
|
UserInfoKey.containerExpandedWindowState: containerExpandedWindowState,
|
|
|
|
|
UserInfoKey.readArticlesFilterState: readArticlesFilterState
|
2019-11-26 23:33:11 +01:00
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
|
2019-11-14 22:06:32 +01:00
|
|
|
|
guard let userInfo = userInfo,
|
2019-11-27 18:43:36 +01:00
|
|
|
|
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
|
2019-11-23 19:30:18 +01:00
|
|
|
|
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
|
2019-11-14 22:06:32 +01:00
|
|
|
|
return
|
2019-08-28 00:43:15 +02:00
|
|
|
|
}
|
2019-11-14 22:06:32 +01:00
|
|
|
|
|
2019-11-28 19:40:33 +01:00
|
|
|
|
treeControllerDelegate.addFilterException(feedIdentifier)
|
|
|
|
|
|
2019-11-23 19:30:18 +01:00
|
|
|
|
switch feedIdentifier {
|
2019-11-14 22:06:32 +01:00
|
|
|
|
|
2019-11-23 19:30:18 +01:00
|
|
|
|
case .smartFeed:
|
|
|
|
|
guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return }
|
2020-02-18 02:40:40 +01:00
|
|
|
|
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(SmartFeedsController.shared)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
rebuildBackingStores() {
|
|
|
|
|
self.treeControllerDelegate.resetFilterExceptions()
|
|
|
|
|
if let indexPath = self.indexPathFor(smartFeed) {
|
|
|
|
|
self.selectFeed(indexPath: indexPath) {
|
|
|
|
|
self.masterFeedViewController.focus()
|
|
|
|
|
}
|
2019-12-09 02:25:22 +01:00
|
|
|
|
}
|
2019-11-14 22:06:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case .script:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
case .folder(let accountID, let folderName):
|
2020-02-18 02:40:40 +01:00
|
|
|
|
guard let accountNode = self.findAccountNode(accountID: accountID),
|
|
|
|
|
let account = accountNode.representedObject as? Account else {
|
2019-11-14 22:06:32 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-02-18 02:40:40 +01:00
|
|
|
|
|
2020-02-18 18:30:58 +01:00
|
|
|
|
markExpanded(account)
|
2020-02-18 02:40:40 +01:00
|
|
|
|
|
|
|
|
|
rebuildBackingStores() {
|
|
|
|
|
self.treeControllerDelegate.resetFilterExceptions()
|
|
|
|
|
|
|
|
|
|
if let folderNode = self.findFolderNode(folderName: folderName, beginningAt: accountNode), let indexPath = self.indexPathFor(folderNode) {
|
|
|
|
|
self.selectFeed(indexPath: indexPath) {
|
|
|
|
|
self.masterFeedViewController.focus()
|
|
|
|
|
}
|
2019-12-09 02:25:22 +01:00
|
|
|
|
}
|
2019-11-14 22:06:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
case .webFeed(let accountID, let webFeedID):
|
2020-02-04 20:00:55 +01:00
|
|
|
|
guard let accountNode = findAccountNode(accountID: accountID),
|
|
|
|
|
let account = accountNode.representedObject as? Account,
|
|
|
|
|
let webFeed = account.existingWebFeed(withWebFeedID: webFeedID) else {
|
2019-11-14 22:06:32 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-02-04 20:00:55 +01:00
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
self.discloseWebFeed(webFeed) {
|
2020-02-04 20:00:55 +01:00
|
|
|
|
self.masterFeedViewController.focus()
|
|
|
|
|
}
|
2019-08-28 00:43:15 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
func handleReadArticle(_ userInfo: [AnyHashable : Any]?) {
|
2019-11-16 01:26:52 +01:00
|
|
|
|
guard let userInfo = userInfo else { return }
|
|
|
|
|
|
|
|
|
|
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
2019-11-14 22:06:32 +01:00
|
|
|
|
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
|
|
|
|
let accountName = articlePathUserInfo[ArticlePathKey.accountName] as? String,
|
2019-11-15 03:11:41 +01:00
|
|
|
|
let webFeedID = articlePathUserInfo[ArticlePathKey.webFeedID] as? String,
|
2019-11-29 21:31:15 +01:00
|
|
|
|
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String,
|
|
|
|
|
let accountNode = findAccountNode(accountID: accountID, accountName: accountName),
|
|
|
|
|
let account = accountNode.representedObject as? Account else {
|
2019-11-14 22:06:32 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-29 21:31:15 +01:00
|
|
|
|
|
|
|
|
|
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
2019-11-14 22:06:32 +01:00
|
|
|
|
|
2019-11-28 19:40:33 +01:00
|
|
|
|
if restoreFeedSelection(userInfo, accountID: accountID, webFeedID: webFeedID, articleID: articleID) {
|
2019-11-16 01:26:52 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
guard let webFeed = account.existingWebFeed(withWebFeedID: webFeedID) else {
|
|
|
|
|
return
|
2020-02-04 20:00:55 +01:00
|
|
|
|
}
|
2019-11-28 19:40:33 +01:00
|
|
|
|
|
2020-02-18 02:40:40 +01:00
|
|
|
|
discloseWebFeed(webFeed) {
|
2019-11-16 01:26:52 +01:00
|
|
|
|
self.selectArticleInCurrentFeed(articleID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-28 19:40:33 +01:00
|
|
|
|
func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
|
2019-11-27 18:43:36 +01:00
|
|
|
|
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
|
2019-11-23 19:30:18 +01:00
|
|
|
|
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
|
2019-11-16 01:26:52 +01:00
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-23 19:30:18 +01:00
|
|
|
|
switch feedIdentifier {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
|
2019-11-23 19:30:18 +01:00
|
|
|
|
case .smartFeed:
|
|
|
|
|
guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false }
|
2019-11-28 19:40:33 +01:00
|
|
|
|
if let indexPath = indexPathFor(smartFeed) {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
selectFeed(indexPath: indexPath) {
|
2019-11-28 19:40:33 +01:00
|
|
|
|
self.selectArticleInCurrentFeed(articleID)
|
2019-11-16 01:26:52 +01:00
|
|
|
|
}
|
2019-11-28 19:40:33 +01:00
|
|
|
|
treeControllerDelegate.addFilterException(feedIdentifier)
|
|
|
|
|
return true
|
2019-08-25 02:31:29 +02:00
|
|
|
|
}
|
2019-11-16 01:26:52 +01:00
|
|
|
|
|
|
|
|
|
case .script:
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
case .folder(let accountID, let folderName):
|
|
|
|
|
guard let accountNode = findAccountNode(accountID: accountID),
|
2019-11-28 19:40:33 +01:00
|
|
|
|
let folderNode = findFolderNode(folderName: folderName, beginningAt: accountNode) else {
|
2019-11-16 01:26:52 +01:00
|
|
|
|
return false
|
|
|
|
|
}
|
2019-11-28 19:40:33 +01:00
|
|
|
|
let found = selectFeedAndArticle(feedNode: folderNode, articleID: articleID)
|
|
|
|
|
if found {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedIdentifier)
|
2019-11-16 01:26:52 +01:00
|
|
|
|
}
|
2019-11-28 19:40:33 +01:00
|
|
|
|
return found
|
2019-11-16 01:26:52 +01:00
|
|
|
|
|
|
|
|
|
case .webFeed:
|
2019-11-19 18:16:43 +01:00
|
|
|
|
guard let accountNode = findAccountNode(accountID: accountID), let webFeedNode = findWebFeedNode(webFeedID: webFeedID, beginningAt: accountNode) else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2019-11-28 19:40:33 +01:00
|
|
|
|
let found = selectFeedAndArticle(feedNode: webFeedNode, articleID: articleID)
|
|
|
|
|
if found {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedIdentifier)
|
2020-02-04 20:00:55 +01:00
|
|
|
|
if let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.feedID {
|
|
|
|
|
treeControllerDelegate.addFilterException(folderFeedID)
|
|
|
|
|
}
|
2019-11-28 19:40:33 +01:00
|
|
|
|
}
|
|
|
|
|
return found
|
|
|
|
|
|
2019-08-25 02:31:29 +02:00
|
|
|
|
}
|
2019-11-16 01:26:52 +01:00
|
|
|
|
|
|
|
|
|
return false
|
2019-08-25 02:31:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-14 22:06:32 +01:00
|
|
|
|
func findAccountNode(accountID: String, accountName: String? = nil) -> Node? {
|
2019-08-25 02:31:29 +02:00
|
|
|
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-14 22:06:32 +01:00
|
|
|
|
if let accountName = accountName, let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) {
|
2019-08-25 02:31:29 +02:00
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-14 22:06:32 +01:00
|
|
|
|
func findFolderNode(folderName: String, beginningAt startingNode: Node) -> Node? {
|
2019-08-28 00:43:15 +02:00
|
|
|
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Folder)?.nameForDisplay == folderName }) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
func findWebFeedNode(webFeedID: String, beginningAt startingNode: Node) -> Node? {
|
|
|
|
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? WebFeed)?.webFeedID == webFeedID }) {
|
2019-08-25 02:31:29 +02:00
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-19 18:16:43 +01:00
|
|
|
|
func selectFeedAndArticle(feedNode: Node, articleID: String) -> Bool {
|
|
|
|
|
if let feedIndexPath = indexPathFor(feedNode) {
|
2020-02-18 02:40:40 +01:00
|
|
|
|
selectFeed(indexPath: feedIndexPath) {
|
2019-11-19 18:16:43 +01:00
|
|
|
|
self.selectArticleInCurrentFeed(articleID)
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 01:26:52 +01:00
|
|
|
|
func selectArticleInCurrentFeed(_ articleID: String) {
|
|
|
|
|
if let article = self.articles.first(where: { $0.articleID == articleID }) {
|
|
|
|
|
self.selectArticle(article)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 20:35:12 +02:00
|
|
|
|
}
|