From 7b8bed9ac220679eb61957a4d10320a96d4e974e Mon Sep 17 00:00:00 2001 From: everhardt Date: Wed, 27 Oct 2021 20:52:26 +0200 Subject: [PATCH 1/8] Add mark as read on scroll functionality Does not yet include a setting --- .../Timeline/TimelineViewController.swift | 22 +++++++++++++++++++ .../MasterTimelineViewController.swift | 18 +++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 38cec256e..d29c91a8d 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -191,6 +191,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr private let keyboardDelegate = TimelineKeyboardDelegate() private var timelineShowsSeparatorsObserver: NSKeyValueObservation? + 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 @@ -223,6 +225,12 @@ 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) + } + didRegisterForNotifications = true } } @@ -324,6 +332,20 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } } + @objc func scrollViewDidScroll(notification: Notification){ + scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + } + + @objc func scrollPositionDidChange(){ + let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location + guard let unreadArticlesScrolledAway = articles.articlesAbove(position: firstVisibleRowIndex).unreadArticles() else { return } + + guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: unreadArticlesScrolledAway, markingRead: true, undoManager: undoManager) else { + return + } + runCommand(markReadCommand) + } + @IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) { guard !selectedArticles.isEmpty else { return diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 72a019828..73b39ee99 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -516,6 +516,24 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner @objc func scrollPositionDidChange() { coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow() + + guard let firstVisibleRowIndexPath = tableView.indexPathsForVisibleRows?[0] else { return } + + guard let firstVisibleArticle = dataSource.itemIdentifier(for: firstVisibleRowIndexPath) else { + return + } + + guard let unreadArticlesScrolledAway = coordinator.articles.articlesAbove(article: firstVisibleArticle).unreadArticles() else { return } + + coordinator.markAllAsRead(unreadArticlesScrolledAway) + + for article in unreadArticlesScrolledAway { + if let indexPath = dataSource.indexPath(for: article) { + if let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell { + configure(cell, article: article) + } + } + } } // MARK: Reloading From b8cae328f58030a425473c7308b4b197ea798fd0 Mon Sep 17 00:00:00 2001 From: everhardt Date: Wed, 27 Oct 2021 21:17:04 +0200 Subject: [PATCH 2/8] Add iOS setting for mark as read on scroll --- iOS/AppDefaults.swift | 11 ++++++ .../MasterTimelineViewController.swift | 6 +++- iOS/Settings/Settings.storyboard | 34 +++++++++++++++++++ iOS/Settings/SettingsViewController.swift | 16 +++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index c7a01f33a..17ffbeb1a 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -46,6 +46,7 @@ final class AppDefaults { static let firstRunDate = "firstRunDate" static let timelineGroupByFeed = "timelineGroupByFeed" static let refreshClearsReadArticles = "refreshClearsReadArticles" + static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll" static let timelineNumberOfLines = "timelineNumberOfLines" static let timelineIconDimension = "timelineIconSize" static let timelineSortDirection = "timelineSortDirection" @@ -159,6 +160,15 @@ final class AppDefaults { } } + var markArticlesAsReadOnScroll: Bool { + get { + return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll) + } + set { + AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue) + } + } + var timelineSortDirection: ComparisonResult { get { return AppDefaults.sortDirection(for: Key.timelineSortDirection) @@ -236,6 +246,7 @@ final class AppDefaults { let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, Key.timelineGroupByFeed: false, Key.refreshClearsReadArticles: false, + Key.markArticlesAsReadOnScroll: false, Key.timelineNumberOfLines: 2, Key.timelineIconDimension: IconSize.medium.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 73b39ee99..024504980 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -516,7 +516,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner @objc func scrollPositionDidChange() { coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow() - + + if !AppDefaults.shared.markArticlesAsReadOnScroll { + return + } + guard let firstVisibleRowIndexPath = tableView.indexPathsForVisibleRows?[0] else { return } guard let firstVisibleArticle = dataSource.itemIdentifier(for: firstVisibleRowIndexPath) else { diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 9cb9246b5..e77cb6ce0 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -237,6 +237,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -617,6 +650,7 @@ + diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index 23811c861..0d39bbe15 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -19,6 +19,7 @@ class SettingsViewController: UITableViewController { @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! @@ -66,6 +67,13 @@ class SettingsViewController: UITableViewController { } else { refreshClearsReadArticlesSwitch.isOn = false } + + + if AppDefaults.shared.markArticlesAsReadOnScroll { + markArticlesAsReadOnScrollSwitch.isOn = true + } else { + markArticlesAsReadOnScrollSwitch.isOn = false + } articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name @@ -326,6 +334,14 @@ class SettingsViewController: UITableViewController { } } + @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 From 3db1b60b65c9154b0ce47c7af6f504e82b122bf8 Mon Sep 17 00:00:00 2001 From: everhardt Date: Wed, 27 Oct 2021 22:03:38 +0200 Subject: [PATCH 3/8] Add Mac setting for mark as read on scroll --- Mac/AppDefaults.swift | 11 ++ Mac/Base.lproj/Preferences.storyboard | 100 ++++++++++++------ .../Timeline/TimelineViewController.swift | 3 + 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index df9b7c618..9a1ce53f0 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -33,6 +33,7 @@ final class AppDefaults { static let detailFontSize = "detailFontSize" static let openInBrowserInBackground = "openInBrowserInBackground" static let subscribeToFeedsInDefaultBrowser = "subscribeToFeedsInDefaultBrowser" + static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll" static let articleTextSize = "articleTextSize" static let refreshInterval = "refreshInterval" static let addWebFeedAccountID = "addWebFeedAccountID" @@ -289,6 +290,16 @@ final class AppDefaults { return AppDefaults.bool(for: Key.timelineShowsSeparators) } + + var markArticlesAsReadOnScroll: Bool { + get { + return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll) + } + set { + AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue) + } + } + var articleTextSize: ArticleTextSize { get { let rawValue = UserDefaults.standard.integer(forKey: Key.articleTextSize) diff --git a/Mac/Base.lproj/Preferences.storyboard b/Mac/Base.lproj/Preferences.storyboard index bbe72eefa..051bc1082 100644 --- a/Mac/Base.lproj/Preferences.storyboard +++ b/Mac/Base.lproj/Preferences.storyboard @@ -1,8 +1,8 @@ - + - + @@ -31,23 +31,31 @@ - - + + - + - + + + + + + + + + - + @@ -76,7 +84,7 @@ - + @@ -91,7 +99,7 @@ - + - + @@ -112,7 +120,7 @@ - + @@ -127,7 +135,7 @@ - + @@ -155,10 +163,10 @@ - + - + @@ -166,7 +174,7 @@ - + @@ -201,7 +209,7 @@ - + @@ -209,7 +217,7 @@ - + + @@ -251,11 +279,14 @@ + + - + + @@ -268,6 +299,7 @@ + @@ -282,6 +314,8 @@ + + @@ -307,7 +341,7 @@ - + @@ -487,16 +521,16 @@ - + - + - + - + @@ -603,7 +637,7 @@ - + @@ -658,16 +692,16 @@ - + - + - + - + @@ -770,7 +804,7 @@ - + diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index d29c91a8d..58a792900 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -337,6 +337,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } @objc func scrollPositionDidChange(){ + if !AppDefaults.shared.markArticlesAsReadOnScroll { + return + } let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location guard let unreadArticlesScrolledAway = articles.articlesAbove(position: firstVisibleRowIndex).unreadArticles() else { return } From 8bce42df569f8228707ad0059cdf5f835a7d0cc9 Mon Sep 17 00:00:00 2001 From: everhardt Date: Wed, 27 Oct 2021 22:21:42 +0200 Subject: [PATCH 4/8] Change scrollPositionQueue in Mac TimelineViewController to private --- Mac/MainWindow/Timeline/TimelineViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 58a792900..c9a877fc7 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -191,7 +191,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr private let keyboardDelegate = TimelineKeyboardDelegate() private var timelineShowsSeparatorsObserver: NSKeyValueObservation? - let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0) + private let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0) convenience init(delegate: TimelineDelegate) { self.init(nibName: "TimelineTableView", bundle: nil) From 3b6a3cf4e7645e550bea89644dc660f280bc340f Mon Sep 17 00:00:00 2001 From: everhardt Date: Sat, 30 Oct 2021 10:37:10 +0200 Subject: [PATCH 5/8] Do not mark articles as read on scroll when they were manually toggled --- .../TimelineViewController+ContextualMenus.swift | 3 +++ .../Timeline/TimelineViewController.swift | 14 +++++++++++++- .../MasterTimelineViewController.swift | 5 ++++- iOS/SceneCoordinator.swift | 8 ++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index dd83afdcb..42ec68d95 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -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) { diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index c9a877fc7..0190f1fb6 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -138,6 +138,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } var undoableCommands = [UndoableCommand]() + + var articlesWithManuallyChangedReadStatus: Set
= Set() + private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() private var exceptionArticleFetcher: ArticleFetcher? @@ -341,7 +344,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr return } let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location - guard let unreadArticlesScrolledAway = articles.articlesAbove(position: firstVisibleRowIndex).unreadArticles() else { return } + 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 @@ -370,6 +375,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr return } runCommand(markReadCommand) + for article in selectedArticles { + articlesWithManuallyChangedReadStatus.insert(article) + } } @IBAction func markSelectedArticlesAsUnread(_ sender: Any?) { @@ -377,6 +385,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr return } runCommand(markUnreadCommand) + for article in selectedArticles { + articlesWithManuallyChangedReadStatus.insert(article) + } } @IBAction func copy(_ sender: Any?) { @@ -928,6 +939,7 @@ extension TimelineViewController: NSTableViewDelegate { return } self.runCommand(markUnreadCommand) + articlesWithManuallyChangedReadStatus.insert(article) } private func toggleArticleStarred(_ article: Article) { diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 024504980..195530545 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -527,7 +527,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner return } - guard let unreadArticlesScrolledAway = coordinator.articles.articlesAbove(article: firstVisibleArticle).unreadArticles() else { return } + guard let unreadArticlesScrolledAway = coordinator.articles + .articlesAbove(article: firstVisibleArticle) + .filter({ !coordinator.articlesWithManuallyChangedReadStatus.contains($0) }) + .unreadArticles() else { return } coordinator.markAllAsRead(unreadArticlesScrolledAway) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 2f411e3be..0e9ab2237 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -182,6 +182,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { private(set) var showFeedNames = ShowFeedName.none private(set) var showIcons = false + var articlesWithManuallyChangedReadStatus: Set
= Set() + var prevFeedIndexPath: IndexPath? { guard let indexPath = currentFeedIndexPath else { return nil @@ -783,6 +785,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { return } + articlesWithManuallyChangedReadStatus.removeAll() + currentFeedIndexPath = indexPath masterFeedViewController.updateFeedSelection(animations: animations) @@ -1073,24 +1077,28 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { func markAsReadForCurrentArticle() { if let article = currentArticle { markArticlesWithUndo([article], statusKey: .read, flag: true) + articlesWithManuallyChangedReadStatus.insert(article) } } func markAsUnreadForCurrentArticle() { if let article = currentArticle { markArticlesWithUndo([article], statusKey: .read, flag: false) + articlesWithManuallyChangedReadStatus.insert(article) } } func toggleReadForCurrentArticle() { if let article = currentArticle { toggleRead(article) + articlesWithManuallyChangedReadStatus.insert(article) } } func toggleRead(_ article: Article) { guard !article.status.read || article.isAvailableToMarkUnread else { return } markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read) + articlesWithManuallyChangedReadStatus.insert(article) } func toggleStarredForCurrentArticle() { From 5364b4f384e7fc35cced781a80256c47e6f063b2 Mon Sep 17 00:00:00 2001 From: everhardt Date: Sat, 30 Oct 2021 10:58:12 +0200 Subject: [PATCH 6/8] Mark bottom items in feed as read after 2 seconds on Mac In case markArticlesAsReadOnScroll is set --- .../Timeline/TimelineViewController.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 0190f1fb6..651d74f8c 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -70,6 +70,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr if showsSearchResults { fetchAndReplaceArticlesAsync() } else { + resetMarkAsReadOnScroll() fetchAndReplaceArticlesSync() if articles.count > 0 { tableView.scrollRowToVisible(0) @@ -195,6 +196,8 @@ 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) @@ -292,6 +295,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 @@ -343,6 +348,32 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr 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) } @@ -354,6 +385,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr runCommand(markReadCommand) } + func resetMarkAsReadOnScroll() { + articlesWithManuallyChangedReadStatus.removeAll() + markBottomArticlesAsReadWorkItem?.cancel() + } + @IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) { guard !selectedArticles.isEmpty else { return From 8e5376803372ddf89a7103f91bf2e1af45047f6d Mon Sep 17 00:00:00 2001 From: everhardt Date: Sat, 30 Oct 2021 11:17:59 +0200 Subject: [PATCH 7/8] Mark bottom items in feed as read after 2 seconds on iOS In case markArticlesAsReadOnScroll is set --- .../MasterTimelineViewController.swift | 22 +++++++++++++++++++ iOS/SceneCoordinator.swift | 2 ++ 2 files changed, 24 insertions(+) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 195530545..0553fa58b 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -521,6 +521,28 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner 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 firstVisibleRowIndexPath = tableView.indexPathsForVisibleRows?[0] else { return } guard let firstVisibleArticle = dataSource.itemIdentifier(for: firstVisibleRowIndexPath) else { diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 0e9ab2237..c03018039 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -183,6 +183,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { private(set) var showIcons = false var articlesWithManuallyChangedReadStatus: Set
= Set() + var markBottomArticlesAsReadWorkItem: DispatchWorkItem? var prevFeedIndexPath: IndexPath? { guard let indexPath = currentFeedIndexPath else { @@ -786,6 +787,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } articlesWithManuallyChangedReadStatus.removeAll() + markBottomArticlesAsReadWorkItem?.cancel() currentFeedIndexPath = indexPath masterFeedViewController.updateFeedSelection(animations: animations) From 96ffa3aa22bff6bf575e6377a2805d62aea357cc Mon Sep 17 00:00:00 2001 From: everhardt Date: Sat, 30 Oct 2021 18:57:04 +0200 Subject: [PATCH 8/8] Do not mark as read without user interaction --- .../Timeline/TimelineViewController.swift | 18 +++++++++++++++++- .../MasterTimelineViewController.swift | 4 +++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 651d74f8c..3234ca050 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -142,6 +142,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr var articlesWithManuallyChangedReadStatus: Set
= Set() + private var isScrolling = false + private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() private var exceptionArticleFetcher: ArticleFetcher? @@ -235,6 +237,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr 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 @@ -341,7 +347,17 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } @objc func scrollViewDidScroll(notification: Notification){ - scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + if isScrolling { + scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + } + } + + @objc func scrollViewWillStartLiveScroll(notification: Notification){ + isScrolling = true + } + + @objc func scrollViewDidEndLiveScroll(notification: Notification){ + isScrolling = false } @objc func scrollPositionDidChange(){ diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 0553fa58b..4b05cea59 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -418,7 +418,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func scrollViewDidScroll(_ scrollView: UIScrollView) { - scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + if scrollView.isTracking { + scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + } } // MARK: Notifications