diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 3234ca050..02e057597 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -141,7 +141,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr var undoableCommands = [UndoableCommand]() var articlesWithManuallyChangedReadStatus: Set
= Set() - + private var isScrolling = false private var fetchSerialNumber = 0 @@ -198,8 +198,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr private var timelineShowsSeparatorsObserver: NSKeyValueObservation? private let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0) - - private var markBottomArticlesAsReadWorkItem: DispatchWorkItem? convenience init(delegate: TimelineDelegate) { self.init(nibName: "TimelineTableView", bundle: nil) @@ -365,30 +363,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr return } - // Mark all articles as read when the bottom of the feed is reached - let lastRowIndex = articles.count - 1 - let atBottom = tableView.rows(in: tableView.visibleRect).contains(lastRowIndex) - - if atBottom && markBottomArticlesAsReadWorkItem == nil { - let task = DispatchWorkItem { - let articlesToMarkAsRead = self.articles.filter { !$0.status.read && !self.articlesWithManuallyChangedReadStatus.contains($0) } - - if articlesToMarkAsRead.isEmpty { return } - guard let undoManager = self.undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMarkAsRead, markingRead: true, undoManager: undoManager) else { - return - } - self.runCommand(markReadCommand) - self.markBottomArticlesAsReadWorkItem = nil - } - - markBottomArticlesAsReadWorkItem = task - DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task) - } else if !atBottom, let task = markBottomArticlesAsReadWorkItem { - task.cancel() - markBottomArticlesAsReadWorkItem = nil - } - - // Mark articles scrolled out of sight at the top as read let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location let unreadArticlesScrolledAway = articles.articlesAbove(position: firstVisibleRowIndex).filter { !$0.status.read && !articlesWithManuallyChangedReadStatus.contains($0) } @@ -403,7 +377,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr func resetMarkAsReadOnScroll() { articlesWithManuallyChangedReadStatus.removeAll() - markBottomArticlesAsReadWorkItem?.cancel() } @IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) { diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 104f06f8c..67eca42c8 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -103,6 +103,17 @@ extension Array where Element == Article { return true } + func articlesBetween(upperArticle: Article, lowerArticle: Article) -> [Article] { + guard let upperPosition = firstIndex(of: upperArticle), let lowerPosition = firstIndex(of: lowerArticle) else { return [] } + return articlesBetween(upperPosition: upperPosition, lowerPosition: lowerPosition) + } + + func articlesBetween(upperPosition: Int, lowerPosition: Int) -> [Article] { + guard upperPosition < count, lowerPosition < count else { return [] } + let articlesAbove = self[upperPosition...lowerPosition] + return Array(articlesAbove) + } + func articlesAbove(article: Article) -> [Article] { guard let position = firstIndex(of: article) else { return [] } return articlesAbove(position: position) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 09c08aa7b..2f99ddf69 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -31,6 +31,8 @@ class MasterTimelineViewController: PullUpToMarkAsReadTableViewController, Undoa var undoableCommands = [UndoableCommand]() let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0) + private var firstVisibleArticleWhenDraggingBegan: Article? + private let keyboardManager = KeyboardManager(type: .timeline) override var keyCommands: [UIKeyCommand]? { @@ -250,6 +252,17 @@ class MasterTimelineViewController: PullUpToMarkAsReadTableViewController, Undoa becomeFirstResponder() } + // MARK: - PullUpToMarkAsReadTableViewController + override func pulledUpToMarkAsRead() { + super.pulledUpToMarkAsRead() + + guard let unreadArticles = coordinator.articles + .filter({ !coordinator.articlesWithManuallyChangedReadStatus.contains($0) }) + .unreadArticles() else { return } + + coordinator.markAllAsRead(unreadArticles) + } + // MARK: - Table view override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { @@ -417,6 +430,19 @@ class MasterTimelineViewController: PullUpToMarkAsReadTableViewController, Undoa coordinator.selectArticle(article, animations: [.scroll, .select, .navigation]) } + override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + super.scrollViewWillBeginDragging(scrollView) + + guard let visibleRowIndexPaths = tableView.indexPathsForVisibleRows, visibleRowIndexPaths.count > 0 else { return } + let firstVisibleRowIndexPath = visibleRowIndexPaths[0] + + if scrollView.isTracking { + firstVisibleArticleWhenDraggingBegan = dataSource.itemIdentifier(for: firstVisibleRowIndexPath) + } else { + firstVisibleArticleWhenDraggingBegan = nil + } + } + override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) @@ -525,37 +551,16 @@ class MasterTimelineViewController: PullUpToMarkAsReadTableViewController, Undoa return } - // Mark all articles as read when the bottom of the feed is reached - if let lastVisibleRowIndexPath = tableView.indexPathsForVisibleRows?.last { - let atBottom = dataSource.itemIdentifier(for: lastVisibleRowIndexPath) == coordinator.articles.last - - if atBottom && coordinator.markBottomArticlesAsReadWorkItem == nil { - let task = DispatchWorkItem { - let articlesToMarkAsRead = self.coordinator.articles.filter { !$0.status.read && !self.coordinator.articlesWithManuallyChangedReadStatus.contains($0) } - - if articlesToMarkAsRead.isEmpty { return } - self.coordinator.markAllAsRead(articlesToMarkAsRead) - self.coordinator.markBottomArticlesAsReadWorkItem = nil - } - - coordinator.markBottomArticlesAsReadWorkItem = task - DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task) - } else if !atBottom, let task = coordinator.markBottomArticlesAsReadWorkItem { - task.cancel() - coordinator.markBottomArticlesAsReadWorkItem = nil - } - } - // Mark articles scrolled out of sight at the top as read guard let visibleRowIndexPaths = tableView.indexPathsForVisibleRows, visibleRowIndexPaths.count > 0 else { return } let firstVisibleRowIndexPath = visibleRowIndexPaths[0] - guard let firstVisibleArticle = dataSource.itemIdentifier(for: firstVisibleRowIndexPath) else { + guard let firstVisibleArticle = dataSource.itemIdentifier(for: firstVisibleRowIndexPath), let firstArticleScrolledAway = firstVisibleArticleWhenDraggingBegan else { return } guard let unreadArticlesScrolledAway = coordinator.articles - .articlesAbove(article: firstVisibleArticle) + .articlesBetween(upperArticle: firstArticleScrolledAway, lowerArticle: firstVisibleArticle) .filter({ !coordinator.articlesWithManuallyChangedReadStatus.contains($0) }) .unreadArticles() else { return } diff --git a/iOS/MasterTimeline/PullUpToMarkAsReadTableViewController.swift b/iOS/MasterTimeline/PullUpToMarkAsReadTableViewController.swift index a31d202ce..f207a657f 100644 --- a/iOS/MasterTimeline/PullUpToMarkAsReadTableViewController.swift +++ b/iOS/MasterTimeline/PullUpToMarkAsReadTableViewController.swift @@ -128,7 +128,7 @@ class PullUpToMarkAsReadTableViewController: UITableViewController { } } - markAsRead() + pulledUpToMarkAsRead() } func stopMarkingAsRead() { @@ -139,8 +139,8 @@ class PullUpToMarkAsReadTableViewController: UITableViewController { self.tableView.contentInset = UIEdgeInsets.zero } - func markAsRead() { - // to override by implementation, which should also call stopMarkingAsRead when done + func pulledUpToMarkAsRead() { + // to override by implementation, which should also call stopMarkingAsRead when done or call this DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.stopMarkingAsRead() diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index c8a7f9138..15d8fa1fa 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -183,7 +183,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { private(set) var showIcons = false var articlesWithManuallyChangedReadStatus: Set
= Set() - var markBottomArticlesAsReadWorkItem: DispatchWorkItem? var prevFeedIndexPath: IndexPath? { guard let indexPath = currentFeedIndexPath else { @@ -785,9 +784,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { completion?() return } - articlesWithManuallyChangedReadStatus.removeAll() - markBottomArticlesAsReadWorkItem?.cancel() currentFeedIndexPath = indexPath masterFeedViewController.updateFeedSelection(animations: animations)