diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 048d2b1de..19c3f6824 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -87,7 +87,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner { } } - private var showAvatars = false private var rowHeightWithFeedName: CGFloat = 0.0 private var rowHeightWithoutFeedName: CGFloat = 0.0 diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 41bd737b8..8013d80e4 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; 5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; }; + 5126EE97226CB48A00C22AFC /* AppModelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* AppModelController.swift */; }; 5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; }; 5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; @@ -602,6 +603,7 @@ 51121AA12265430A00BC0EC1 /* NetNewsWire_iOS_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOS_target.xcconfig; sourceTree = ""; }; 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = ""; }; 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = ""; }; + 5126EE96226CB48A00C22AFC /* AppModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModelController.swift; sourceTree = ""; }; 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailKeyboardDelegate.swift; sourceTree = ""; }; 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = ""; }; 512E08F722688F7C00BDCFDD /* MasterTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTableViewSectionHeader.swift; sourceTree = ""; }; @@ -1512,6 +1514,7 @@ 840D617E2029031C009BC708 /* AppDelegate.swift */, 51C45254226507D200C03939 /* AppAssets.swift */, 51C45255226507D200C03939 /* AppDefaults.swift */, + 5126EE96226CB48A00C22AFC /* AppModelController.swift */, 51C4525D226508F600C03939 /* Master */, 51C4526D2265091600C03939 /* Timeline */, 51C4527D2265092C00C03939 /* Detail */, @@ -1784,12 +1787,12 @@ ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { 6581C73220CED60000F4AD34 = { - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Manual; }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; - DevelopmentTeam = 9C84TZ7Q6Z; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; }; 840D61902029031D009BC708 = { @@ -1800,7 +1803,7 @@ }; 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.HardenedRuntime = { @@ -1810,7 +1813,7 @@ }; 849C64701ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = 9C84TZ7Q6Z; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; TestTargetID = 849C645F1ED37A5D003D8FC0; }; @@ -2146,6 +2149,7 @@ 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */, 51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */, 51C4526B226508F600C03939 /* MasterViewController.swift in Sources */, + 5126EE97226CB48A00C22AFC /* AppModelController.swift in Sources */, 51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */, 51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, diff --git a/iOS/AppModelController.swift b/iOS/AppModelController.swift new file mode 100644 index 000000000..96b434820 --- /dev/null +++ b/iOS/AppModelController.swift @@ -0,0 +1,244 @@ +// +// NavigationModelController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 4/21/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation +import Account +import Articles +import RSCore + +public extension Notification.Name { + static let ShowFeedNamesDidChange = Notification.Name(rawValue: "ShowFeedNamesDidChange") + static let ArticlesReinitialized = Notification.Name(rawValue: "ArticlesReinitialized") + static let ArticleDataDidChange = Notification.Name(rawValue: "ArticleDataDidChange") + static let ArticlesDidChange = Notification.Name(rawValue: "ArticlesDidChange") +} + +class AppModelController { + + static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) + } + + private var sortDirection = AppDefaults.timelineSortDirection { + didSet { + if sortDirection != oldValue { + sortDirectionDidChange() + } + } + } + + var showFeedNames = false { + didSet { + NotificationCenter.default.post(name: .ShowFeedNamesDidChange, object: self, userInfo: nil) + } + } + var showAvatars = false + + var timelineFetcher: ArticleFetcher? { + didSet { + if timelineFetcher is Feed { + showFeedNames = false + } else { + showFeedNames = true + } + fetchArticles() + NotificationCenter.default.post(name: .ArticlesReinitialized, object: self, userInfo: nil) + } + } + + var articles = ArticleArray() { + didSet { + if articles == oldValue { + return + } + if articles.representSameArticlesInSameOrder(as: oldValue) { + articleRowMap = [String: Int]() + NotificationCenter.default.post(name: .ArticleDataDidChange, object: self, userInfo: nil) + return + } + updateShowAvatars() + articleRowMap = [String: Int]() + NotificationCenter.default.post(name: .ArticlesDidChange, object: self, userInfo: nil) + } + } + + private var articleRowMap = [String: Int]() // articleID: rowIndex + + // MARK: Notifications + + @objc func userDefaultsDidChange(_ note: Notification) { + self.sortDirection = AppDefaults.timelineSortDirection + } + + @objc func accountDidDownloadArticles(_ note: Notification) { + + guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set else { + return + } + + let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds) || representedObjectsContainsAnyPseudoFeed() + if shouldFetchAndMergeArticles { + queueFetchAndMergeArticles() + } + + } + + // MARK: API + + func indexesForArticleIDs(_ articleIDs: Set) -> IndexSet { + + var indexes = IndexSet() + + articleIDs.forEach { (articleID) in + guard let oneIndex = row(for: articleID) else { + return + } + if oneIndex != NSNotFound { + indexes.insert(oneIndex) + } + } + + return indexes + } + +} + +private extension AppModelController { + + // MARK: Fetching Articles + + func fetchArticles() { + + guard let timelineFetcher = timelineFetcher else { + articles = ArticleArray() + return + } + + let fetchedArticles = timelineFetcher.fetchArticles() + updateArticles(with: fetchedArticles) + + } + + func emptyTheTimeline() { + if !articles.isEmpty { + articles = [Article]() + } + } + + func sortDirectionDidChange() { + updateArticles(with: Set(articles)) + } + + func updateArticles(with unsortedArticles: Set
) { + let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection) + if articles != sortedArticles { + articles = sortedArticles + } + } + + func row(for articleID: String) -> Int? { + updateArticleRowMapIfNeeded() + return articleRowMap[articleID] + } + + func updateArticleRowMap() { + var rowMap = [String: Int]() + var index = 0 + articles.forEach { (article) in + rowMap[article.articleID] = index + index += 1 + } + articleRowMap = rowMap + } + + func updateArticleRowMapIfNeeded() { + if articleRowMap.isEmpty { + updateArticleRowMap() + } + } + + func queueFetchAndMergeArticles() { + AppModelController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) + } + + @objc func fetchAndMergeArticles() { + + guard let timelineFetcher = timelineFetcher else { + return + } + + var unsortedArticles = timelineFetcher.fetchArticles() + + // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. + let unsortedArticleIDs = unsortedArticles.articleIDs() + for article in articles { + if !unsortedArticleIDs.contains(article.articleID) { + unsortedArticles.insert(article) + } + } + + updateArticles(with: unsortedArticles) + + } + + func representedObjectsContainsAnyPseudoFeed() -> Bool { + if timelineFetcher is PseudoFeed { + return true + } + return false + } + + func representedObjectsContainsAnyFeed(_ feeds: Set) -> Bool { + + // Return true if there’s a match or if a folder contains (recursively) one of feeds + + if let feed = timelineFetcher as? Feed { + for oneFeed in feeds { + if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url { + return true + } + } + } else if let folder = timelineFetcher as? Folder { + for oneFeed in feeds { + if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) { + return true + } + } + } + + return false + + } + + // MARK: Misc + + func updateShowAvatars() { + + if showFeedNames { + self.showAvatars = true + return + } + + for article in articles { + if let authors = article.authors { + for author in authors { + if author.avatarURL != nil { + self.showAvatars = true + return + } + } + } + } + + self.showAvatars = false + } + +} diff --git a/iOS/Master/MasterViewController.swift b/iOS/Master/MasterViewController.swift index 92e088c9f..814b45bea 100644 --- a/iOS/Master/MasterViewController.swift +++ b/iOS/Master/MasterViewController.swift @@ -20,6 +20,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { var expandedNodes = [Node]() var shadowTable = [[Node]]() + let appModelController = AppModelController() let treeControllerDelegate = FeedTreeControllerDelegate() lazy var treeController: TreeController = { return TreeController(delegate: treeControllerDelegate) @@ -267,21 +268,16 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) - if let pseudoFeed = node.representedObject as? PseudoFeed { - timeline.title = pseudoFeed.nameForDisplay - timeline.representedObjects = [pseudoFeed] + if let fetcher = node.representedObject as? ArticleFetcher { + appModelController.timelineFetcher = fetcher } - if let folder = node.representedObject as? Folder { - timeline.title = folder.nameForDisplay - timeline.representedObjects = [folder] - } - - if let feed = node.representedObject as? Feed { - timeline.title = feed.nameForDisplay - timeline.representedObjects = [feed] + if let nameProvider = node.representedObject as? DisplayNameProvider { + timeline.title = nameProvider.nameForDisplay } + timeline.appModelController = appModelController + self.navigationController?.pushViewController(timeline, animated: true) } diff --git a/iOS/Timeline/MasterTimelineViewController.swift b/iOS/Timeline/MasterTimelineViewController.swift index 35706c4d6..637322b10 100644 --- a/iOS/Timeline/MasterTimelineViewController.swift +++ b/iOS/Timeline/MasterTimelineViewController.swift @@ -13,18 +13,16 @@ import Articles class MasterTimelineViewController: UITableViewController, UndoableCommandRunner { - var undoableCommands = [UndoableCommand]() - - private var showAvatars = false private var rowHeightWithFeedName: CGFloat = 0.0 private var rowHeightWithoutFeedName: CGFloat = 0.0 private var currentRowHeight: CGFloat { - return showFeedNames ? rowHeightWithFeedName : rowHeightWithoutFeedName + return appModelController.showFeedNames ? rowHeightWithFeedName : rowHeightWithoutFeedName } - static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) - + var appModelController: AppModelController! + var undoableCommands = [UndoableCommand]() + var detailViewController: DetailViewController? { if let split = splitViewController { let controllers = split.viewControllers @@ -32,69 +30,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } return nil } - - var representedObjects: [AnyObject]? { - didSet { - if !representedObjectArraysAreEqual(oldValue, representedObjects) { - - if let representedObjects = representedObjects { - if representedObjects.count == 1 && representedObjects.first is Feed { - showFeedNames = false - } - else { - showFeedNames = true - } - } - else { - showFeedNames = false - } - - fetchArticles() - if articles.count > 0 { - tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) - } - - } - } - } - - var articles = ArticleArray() { - didSet { - if articles == oldValue { - return - } - if articles.representSameArticlesInSameOrder(as: oldValue) { - // When the array is the same — same articles, same order — - // but some data in some of the articles may have changed. - // Just reload visible cells in this case: don’t call reloadData. - articleRowMap = [String: Int]() - reloadAllVisibleCells() - return - } - updateShowAvatars() - articleRowMap = [String: Int]() - tableView.reloadData() - } - } - - private var articleRowMap = [String: Int]() // articleID: rowIndex - private var showFeedNames = false { - didSet { - if showFeedNames != oldValue { - updateShowAvatars() - updateTableViewRowHeight() - } - } - } - - private var sortDirection = AppDefaults.timelineSortDirection { - didSet { - if sortDirection != oldValue { - sortDirectionDidChange() - } - } - } - + override var canBecomeFirstResponder: Bool { return true } @@ -109,10 +45,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: appModelController) + NotificationCenter.default.addObserver(self, selector: #selector(showFeedNamesDidChange(_:)), name: .ShowFeedNamesDidChange, object: appModelController) + NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: appModelController) + NotificationCenter.default.addObserver(self, selector: #selector(articlesDidChange(_:)), name: .ArticlesDidChange, object: appModelController) + refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged) @@ -132,7 +71,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController - let article = articles[indexPath.row] + let article = appModelController.articles[indexPath.row] controller.article = article controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true @@ -156,7 +95,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner let markTitle = NSLocalizedString("Mark All Read", comment: "Mark All Read") let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in - guard let articles = self?.articles, + guard let articles = self?.appModelController.articles, let undoManager = self?.undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { return @@ -177,12 +116,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return articles.count + return appModelController.articles.count } override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let article = articles[indexPath.row] + let article = appModelController.articles[indexPath.row] // Set up the star action let starTitle = article.status.starred ? @@ -226,7 +165,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell - let article = articles[indexPath.row] + let article = appModelController.articles[indexPath.row] configureTimelineCell(cell, article: article) @@ -234,7 +173,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let article = articles[indexPath.row] + let article = appModelController.articles[indexPath.row] if !article.status.read { markArticles(Set([article]), statusKey: .read, flag: true) } @@ -267,7 +206,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner performBlockAndRestoreSelection { tableView.indexPathsForVisibleRows?.forEach { indexPath in - guard let article = articles.articleAtRow(indexPath.row) else { + guard let article = appModelController.articles.articleAtRow(indexPath.row) else { return } @@ -283,14 +222,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner @objc func avatarDidBecomeAvailable(_ note: Notification) { - guard showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { + guard appModelController.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { return } performBlockAndRestoreSelection { tableView.indexPathsForVisibleRows?.forEach { indexPath in - guard let article = articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else { + guard let article = appModelController.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else { return } @@ -306,27 +245,29 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } @objc func imageDidBecomeAvailable(_ note: Notification) { - if showAvatars { + if appModelController.showAvatars { queueReloadVisableCells() } } - @objc func accountDidDownloadArticles(_ note: Notification) { - - guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set else { - return - } - - let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds) || representedObjectsContainsAnyPseudoFeed() - if shouldFetchAndMergeArticles { - queueFetchAndMergeArticles() + @objc func articlesReinitialized(_ note: Notification) { + if appModelController.articles.count > 0 { + tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) } } - @objc func userDefaultsDidChange(_ note: Notification) { - self.sortDirection = AppDefaults.timelineSortDirection + @objc func showFeedNamesDidChange(_ note: Notification) { + updateTableViewRowHeight() } - + + @objc func articleDataDidChange(_ note: Notification) { + reloadAllVisibleCells() + } + + @objc func articlesDidChange(_ note: Notification) { + tableView.reloadData() + } + // MARK: Reloading @objc func reloadAllVisibleCells() { @@ -349,7 +290,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner if articleIDs.isEmpty { return } - let indexes = indexesForArticleIDs(articleIDs) + let indexes = appModelController.indexesForArticleIDs(articleIDs) reloadVisibleCells(for: indexes) } @@ -385,26 +326,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner updateTableViewRowHeight() } - @objc func fetchAndMergeArticles() { - - guard let representedObjects = representedObjects else { - return - } - - var unsortedArticles = fetchUnsortedArticles(for: representedObjects) - - // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. - let unsortedArticleIDs = unsortedArticles.articleIDs() - for article in articles { - if !unsortedArticleIDs.contains(article.articleID) { - unsortedArticles.insert(article) - } - } - - updateArticles(with: unsortedArticles) - - } - } // MARK: Private @@ -423,13 +344,13 @@ private extension MasterTimelineViewController { } let featuredImage = featuredImageFor(article) - cell.cellData = MasterTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: featuredImage) + cell.cellData = MasterTimelineCellData(article: article, showFeedName: appModelController.showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: appModelController.showAvatars, featuredImage: featuredImage) } func avatarFor(_ article: Article) -> UIImage? { - if !showAvatars { + if !appModelController.showAvatars { return nil } @@ -468,73 +389,6 @@ private extension MasterTimelineViewController { tableView.rowHeight = currentRowHeight tableView.estimatedRowHeight = currentRowHeight } - - func updateShowAvatars() { - - if showFeedNames { - self.showAvatars = true - return - } - - for article in articles { - if let authors = article.authors { - for author in authors { - if author.avatarURL != nil { - self.showAvatars = true - return - } - } - } - } - - self.showAvatars = false - } - - func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool { - - if objects1 == nil && objects2 == nil { - return true - } - guard let objects1 = objects1, let objects2 = objects2 else { - return false - } - if objects1.count != objects2.count { - return false - } - - var ix = 0 - for oneObject in objects1 { - if oneObject !== objects2[ix] { - return false - } - ix += 1 - } - return true - } - - // MARK: Fetching Articles - - func fetchArticles() { - - guard let representedObjects = representedObjects else { - emptyTheTimeline() - return - } - - let fetchedArticles = fetchUnsortedArticles(for: representedObjects) - updateArticles(with: fetchedArticles) - - } - - func emptyTheTimeline() { - if !articles.isEmpty { - articles = [Article]() - } - } - - func sortDirectionDidChange() { - updateArticles(with: Set(articles)) - } func performBlockAndRestoreSelection(_ block: (() -> Void)) { let indexPaths = tableView.indexPathsForSelectedRows @@ -543,107 +397,5 @@ private extension MasterTimelineViewController { self?.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) } } - - func updateArticles(with unsortedArticles: Set
) { - - let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection) - if articles != sortedArticles { - articles = sortedArticles - } - - } - - func fetchUnsortedArticles(for representedObjects: [Any]) -> Set
{ - - var fetchedArticles = Set
() - - for object in representedObjects { - - if let articleFetcher = object as? ArticleFetcher { - fetchedArticles.formUnion(articleFetcher.fetchArticles()) - } - } - - return fetchedArticles - } - - func indexesForArticleIDs(_ articleIDs: Set) -> IndexSet { - - var indexes = IndexSet() - - articleIDs.forEach { (articleID) in - guard let oneIndex = row(for: articleID) else { - return - } - if oneIndex != NSNotFound { - indexes.insert(oneIndex) - } - } - - return indexes - } - - func row(for articleID: String) -> Int? { - updateArticleRowMapIfNeeded() - return articleRowMap[articleID] - } - - func updateArticleRowMap() { - var rowMap = [String: Int]() - var index = 0 - articles.forEach { (article) in - rowMap[article.articleID] = index - index += 1 - } - articleRowMap = rowMap - } - - func updateArticleRowMapIfNeeded() { - if articleRowMap.isEmpty { - updateArticleRowMap() - } - } - - func queueFetchAndMergeArticles() { - MasterTimelineViewController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) - } - - func representedObjectsContainsAnyPseudoFeed() -> Bool { - guard let representedObjects = representedObjects else { - return false - } - for representedObject in representedObjects { - if representedObject is PseudoFeed { - return true - } - } - return false - } - - func representedObjectsContainsAnyFeed(_ feeds: Set) -> Bool { - - // Return true if there’s a match or if a folder contains (recursively) one of feeds - - guard let representedObjects = representedObjects else { - return false - } - for representedObject in representedObjects { - if let feed = representedObject as? Feed { - for oneFeed in feeds { - if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url { - return true - } - } - } - else if let folder = representedObject as? Folder { - for oneFeed in feeds { - if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) { - return true - } - } - } - } - return false - } }