mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge pull request #3329 from everhardt/feat-1844-scroll-mark-as-read
Add mark as read on scroll
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -516,6 +518,53 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
|
||||
@objc func scrollPositionDidChange() {
|
||||
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
|
||||
|
||||
if !AppDefaults.shared.markArticlesAsReadOnScroll {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
guard let unreadArticlesScrolledAway = coordinator.articles
|
||||
.articlesAbove(article: firstVisibleArticle)
|
||||
.filter({ !coordinator.articlesWithManuallyChangedReadStatus.contains($0) })
|
||||
.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
|
||||
|
||||
@@ -182,6 +182,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
private(set) var showFeedNames = ShowFeedName.none
|
||||
private(set) var showIcons = false
|
||||
|
||||
var articlesWithManuallyChangedReadStatus: Set<Article> = Set()
|
||||
var markBottomArticlesAsReadWorkItem: DispatchWorkItem?
|
||||
|
||||
var prevFeedIndexPath: IndexPath? {
|
||||
guard let indexPath = currentFeedIndexPath else {
|
||||
return nil
|
||||
@@ -783,6 +786,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
return
|
||||
}
|
||||
|
||||
articlesWithManuallyChangedReadStatus.removeAll()
|
||||
markBottomArticlesAsReadWorkItem?.cancel()
|
||||
|
||||
currentFeedIndexPath = indexPath
|
||||
masterFeedViewController.updateFeedSelection(animations: animations)
|
||||
|
||||
@@ -1073,24 +1079,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() {
|
||||
|
||||
@@ -237,6 +237,39 @@
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="uZo-ol-UQR" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="641" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uZo-ol-UQR" id="riE-Jh-UIr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" text="Mark Articles as Read on Scroll" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7hU-Jg-Gqb">
|
||||
<rect key="frame" x="20" y="11" width="200.5" height="21.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="eIQ-OC-hEm">
|
||||
<rect key="frame" x="307" y="6.5" width="51" height="31"/>
|
||||
<color key="onTintColor" name="primaryAccentColor"/>
|
||||
<connections>
|
||||
<action selector="switchMarkArticlesAsReadOnScroll:" destination="a0p-rk-skQ" eventType="valueChanged" id="EGi-c5-2ic"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="eIQ-OC-hEm" secondAttribute="trailing" constant="18" id="JEv-k0-ayH"/>
|
||||
<constraint firstItem="7hU-Jg-Gqb" firstAttribute="top" secondItem="riE-Jh-UIr" secondAttribute="topMargin" id="P22-FR-r7A"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="7hU-Jg-Gqb" secondAttribute="bottom" id="VRW-kv-1x8"/>
|
||||
<constraint firstItem="eIQ-OC-hEm" firstAttribute="top" relation="greaterThanOrEqual" secondItem="riE-Jh-UIr" secondAttribute="top" constant="6" id="leW-2Y-QI0"/>
|
||||
<constraint firstItem="7hU-Jg-Gqb" firstAttribute="leading" secondItem="riE-Jh-UIr" secondAttribute="leadingMargin" id="q6H-5f-mWN"/>
|
||||
<constraint firstItem="eIQ-OC-hEm" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="7hU-Jg-Gqb" secondAttribute="trailing" constant="8" id="qlX-5H-yYc"/>
|
||||
<constraint firstItem="eIQ-OC-hEm" firstAttribute="centerY" secondItem="riE-Jh-UIr" secondAttribute="centerY" id="rnd-Gr-Eq2"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="eIQ-OC-hEm" secondAttribute="bottom" constant="6" id="zdI-d8-4Pl"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="8Gj-qz-NMY" customClass="VibrantBasicTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="645.5" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
@@ -617,6 +650,7 @@
|
||||
<outlet property="colorPaletteDetailLabel" destination="16m-Ns-Y8V" id="zdj-Ag-ZUw"/>
|
||||
<outlet property="confirmMarkAllAsReadSwitch" destination="UOo-9z-IuL" id="yLZ-Kf-wDt"/>
|
||||
<outlet property="groupByFeedSwitch" destination="JNi-Wz-RbU" id="TwH-Kd-o6N"/>
|
||||
<outlet property="markArticlesAsReadOnScrollSwitch" destination="eIQ-OC-hEm" id="f5D-SM-LGr"/>
|
||||
<outlet property="openLinksInNetNewsWire" destination="dhR-L2-PX3" id="z1b-pX-bwG"/>
|
||||
<outlet property="refreshClearsReadArticlesSwitch" destination="duV-CN-JmH" id="xTd-jF-Ei1"/>
|
||||
<outlet property="showFullscreenArticlesSwitch" destination="2Md-2E-7Z4" id="lEN-VP-wEO"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user