diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift
index 66d952b7d..5caac7432 100644
--- a/Mac/AppDefaults.swift
+++ b/Mac/AppDefaults.swift
@@ -44,6 +44,7 @@ final class AppDefaults {
static let currentThemeName = "currentThemeName"
static let hasSeenNotAllArticlesHaveURLsAlert = "hasSeenNotAllArticlesHaveURLsAlert"
static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown"
+ static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll"
// Hidden prefs
static let showDebugMenu = "ShowDebugMenu"
@@ -329,6 +330,14 @@ final class AppDefaults {
}
}
+ var markArticlesAsReadOnScroll: Bool {
+ get {
+ return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll)
+ }
+ set {
+ AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue)
+ }
+ }
func registerDefaults() {
#if DEBUG
diff --git a/Mac/Base.lproj/Preferences.storyboard b/Mac/Base.lproj/Preferences.storyboard
index e8cd72511..ec3c0ba73 100644
--- a/Mac/Base.lproj/Preferences.storyboard
+++ b/Mac/Base.lproj/Preferences.storyboard
@@ -32,22 +32,30 @@
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
-
+
@@ -76,7 +84,7 @@
-
+
@@ -91,7 +99,7 @@
-
+
-
-
+
+
-
+
@@ -127,7 +135,7 @@
-
+
@@ -155,18 +163,18 @@
-
+
-
-
+
+
-
+
@@ -201,15 +209,15 @@
-
-
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
+
-
-
-
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
+
-
+
+
-
-
-
+
@@ -427,35 +455,40 @@
-
-
-
+
-
+
+
+
+
+
+
-
+
+
+
-
+
@@ -475,7 +508,6 @@
-
@@ -495,16 +527,16 @@
-
+
-
+
-
+
-
+
@@ -579,7 +611,7 @@
-
+
@@ -611,7 +643,7 @@
-
+
@@ -642,7 +674,7 @@
-
+
@@ -666,16 +698,16 @@
-
+
-
+
-
+
-
+
@@ -744,7 +776,7 @@
-
+
@@ -778,7 +810,7 @@
-
+
@@ -809,7 +841,7 @@
-
+
diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift
index 30ce24961..5b3e40542 100644
--- a/Mac/MainWindow/Timeline/TimelineViewController.swift
+++ b/Mac/MainWindow/Timeline/TimelineViewController.swift
@@ -194,6 +194,8 @@ 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)
+
convenience init(delegate: TimelineDelegate) {
self.init(nibName: "TimelineTableView", bundle: nil)
self.delegate = delegate
@@ -224,6 +226,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidDirectMarking(_:)), name: .MarkStatusCommandDidDirectMarking, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidUndoDirectMarking(_:)), name: .MarkStatusCommandDidUndoDirectMarking, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll), name: NSScrollView.didLiveScrollNotification, object: tableView.enclosingScrollView)
didRegisterForNotifications = true
}
}
@@ -328,6 +331,31 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
@objc func openArticleInBrowser(_ sender: Any?) {
let urlStrings = selectedArticles.compactMap { $0.preferredLink }
Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
+
+ if let link = oneSelectedArticle?.preferredLink {
+ Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
+ }
+ }
+
+ @objc func scrollViewDidScroll(notification: Notification) {
+ guard AppDefaults.shared.markArticlesAsReadOnScroll else { return }
+
+ let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location
+
+ // We go back 5 extras incase we didn't get a notification during a fast scroll
+ let indexSet = IndexSet(integersIn: max(firstVisibleRowIndex - 6, 0)...max(firstVisibleRowIndex - 1, 0))
+ guard let articles = articles.articlesForIndexes(indexSet).unreadArticles() else {
+ return
+ }
+
+ let markArticles = articles.filter { !directlyMarkedAsUnreadArticles.contains($0) }
+ guard !markArticles.isEmpty,
+ let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: markArticles, markingRead: true, directlyMarked: false, undoManager: undoManager) else {
+ return
+ }
+
+ runCommand(markReadCommand)
}
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift
index 7f99e8e6e..689784b75 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, upperPosition <= lowerPosition 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/AppDefaults.swift b/iOS/AppDefaults.swift
index 91c355853..604500269 100644
--- a/iOS/AppDefaults.swift
+++ b/iOS/AppDefaults.swift
@@ -59,6 +59,7 @@ final class AppDefaults {
static let useSystemBrowser = "useSystemBrowser"
static let currentThemeName = "currentThemeName"
static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown"
+ static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll"
}
let isDeveloperBuild: Bool = {
@@ -233,6 +234,15 @@ final class AppDefaults {
}
}
+ var markArticlesAsReadOnScroll: Bool {
+ get {
+ return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll)
+ }
+ set {
+ AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue)
+ }
+ }
+
static func registerDefaults() {
let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
Key.timelineGroupByFeed: false,
diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift
index 15f974941..10a744838 100644
--- a/iOS/MasterTimeline/MasterTimelineViewController.swift
+++ b/iOS/MasterTimeline/MasterTimelineViewController.swift
@@ -35,7 +35,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
weak var coordinator: SceneCoordinator!
var undoableCommands = [UndoableCommand]()
- let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0)
private let keyboardManager = KeyboardManager(type: .timeline)
override var keyCommands: [UIKeyCommand]? {
@@ -434,7 +433,25 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
- scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
+ coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
+
+ // Implement Mark As Read on Scroll where we mark after the leading edge goes a little beyond the safe area inset
+ guard AppDefaults.shared.markArticlesAsReadOnScroll,
+ let firstVisibleindexPath = tableView.indexPathsForVisibleRows?.first else { return }
+
+ var articles = [Article]()
+ for i in firstVisibleindexPath.row..()
+ var directlyMarkedAsUnreadArticles = Set()
var prefersStatusBarHidden = false
diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard
index 68ff0e72c..9db7de044 100644
--- a/iOS/Settings/Settings.storyboard
+++ b/iOS/Settings/Settings.storyboard
@@ -254,9 +254,42 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -282,20 +315,20 @@
-
+
-
+
-
+
-
+
-
-
+
-
+
-
+
-
+
@@ -385,10 +418,10 @@
-
+
-
+
@@ -421,14 +454,14 @@
-
+
-
+
@@ -438,7 +471,7 @@
-
+
@@ -455,7 +488,7 @@
-
+
@@ -472,7 +505,7 @@
-
+
@@ -489,7 +522,7 @@
-
+
@@ -506,7 +539,7 @@
-
+
@@ -523,7 +556,7 @@
-
+
@@ -540,7 +573,7 @@
-
+
@@ -557,7 +590,7 @@
-
+
@@ -593,6 +626,7 @@
+
diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift
index 2112e62dd..0867125f6 100644
--- a/iOS/Settings/SettingsViewController.swift
+++ b/iOS/Settings/SettingsViewController.swift
@@ -22,9 +22,9 @@ class SettingsViewController: UITableViewController, Logging {
@IBOutlet weak var timelineSortOrderSwitch: UISwitch!
@IBOutlet weak var groupByFeedSwitch: UISwitch!
@IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch!
+ @IBOutlet weak var markArticlesAsReadOnScrollSwitch: UISwitch!
@IBOutlet weak var articleThemeDetailLabel: UILabel!
@IBOutlet weak var confirmMarkAllAsReadSwitch: UISwitch!
- @IBOutlet weak var showFullscreenArticlesSwitch: UISwitch!
@IBOutlet weak var colorPaletteDetailLabel: UILabel!
@IBOutlet weak var openLinksInNetNewsWire: UISwitch!
@@ -75,6 +75,12 @@ class SettingsViewController: UITableViewController, Logging {
refreshClearsReadArticlesSwitch.isOn = false
}
+ if AppDefaults.shared.markArticlesAsReadOnScroll {
+ markArticlesAsReadOnScrollSwitch.isOn = true
+ } else {
+ markArticlesAsReadOnScrollSwitch.isOn = false
+ }
+
articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
if AppDefaults.shared.confirmMarkAllAsRead {
@@ -331,6 +337,14 @@ class SettingsViewController: UITableViewController, Logging {
}
}
+ @IBAction func switchMarkArticlesAsReadOnScroll(_ sender: Any) {
+ if markArticlesAsReadOnScrollSwitch.isOn {
+ AppDefaults.shared.markArticlesAsReadOnScroll = true
+ } else {
+ AppDefaults.shared.markArticlesAsReadOnScroll = false
+ }
+ }
+
@IBAction func switchConfirmMarkAllAsRead(_ sender: Any) {
if confirmMarkAllAsReadSwitch.isOn {
AppDefaults.shared.confirmMarkAllAsRead = true