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 @@ - + - + - + - + - - + - + -