Merge pull request #3329 from everhardt/feat-1844-scroll-mark-as-read

Add mark as read on scroll
This commit is contained in:
Maurice Parker
2021-11-04 12:04:53 -05:00
committed by GitHub
9 changed files with 291 additions and 34 deletions

View File

@@ -111,6 +111,9 @@ private extension TimelineViewController {
func markArticles(_ articles: [Article], read: Bool) {
markArticles(articles, statusKey: .read, flag: read)
for article in articles {
articlesWithManuallyChangedReadStatus.insert(article)
}
}
func markArticles(_ articles: [Article], starred: Bool) {

View File

@@ -70,6 +70,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
if showsSearchResults {
fetchAndReplaceArticlesAsync()
} else {
resetMarkAsReadOnScroll()
fetchAndReplaceArticlesSync()
if articles.count > 0 {
tableView.scrollRowToVisible(0)
@@ -138,6 +139,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
var undoableCommands = [UndoableCommand]()
var articlesWithManuallyChangedReadStatus: Set<Article> = Set()
private var isScrolling = false
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
private var exceptionArticleFetcher: ArticleFetcher?
@@ -191,6 +197,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
private let keyboardDelegate = TimelineKeyboardDelegate()
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)
self.delegate = delegate
@@ -223,6 +233,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
if let scrollView = self.tableView.enclosingScrollView {
scrollView.contentView.postsBoundsChangedNotifications = true
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll(notification:)), name: NSView.boundsDidChangeNotification, object: scrollView.contentView)
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewWillStartLiveScroll(notification:)), name: NSScrollView.willStartLiveScrollNotification, object: scrollView)
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidEndLiveScroll(notification:)), name: NSScrollView.didEndLiveScrollNotification, object: scrollView)
}
didRegisterForNotifications = true
}
}
@@ -281,6 +301,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
func restoreState(from state: [AnyHashable : Any]) {
resetMarkAsReadOnScroll()
guard let readArticlesFilterStateKeys = state[UserInfoKey.readArticlesFilterStateKeys] as? [[AnyHashable: AnyHashable]],
let readArticlesFilterStateValues = state[UserInfoKey.readArticlesFilterStateValues] as? [Bool] else {
return
@@ -324,6 +346,66 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
}
@objc func scrollViewDidScroll(notification: Notification){
if isScrolling {
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
}
}
@objc func scrollViewWillStartLiveScroll(notification: Notification){
isScrolling = true
}
@objc func scrollViewDidEndLiveScroll(notification: Notification){
isScrolling = false
}
@objc func scrollPositionDidChange(){
if !AppDefaults.shared.markArticlesAsReadOnScroll {
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) }
if unreadArticlesScrolledAway.isEmpty { return }
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: unreadArticlesScrolledAway, markingRead: true, undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
}
func resetMarkAsReadOnScroll() {
articlesWithManuallyChangedReadStatus.removeAll()
markBottomArticlesAsReadWorkItem?.cancel()
}
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
guard !selectedArticles.isEmpty else {
return
@@ -345,6 +427,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return
}
runCommand(markReadCommand)
for article in selectedArticles {
articlesWithManuallyChangedReadStatus.insert(article)
}
}
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
@@ -352,6 +437,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return
}
runCommand(markUnreadCommand)
for article in selectedArticles {
articlesWithManuallyChangedReadStatus.insert(article)
}
}
@IBAction func copy(_ sender: Any?) {
@@ -903,6 +991,7 @@ extension TimelineViewController: NSTableViewDelegate {
return
}
self.runCommand(markUnreadCommand)
articlesWithManuallyChangedReadStatus.insert(article)
}
private func toggleArticleStarred(_ article: Article) {