Merge branch 'main' into ios-ui-settings-localised

# Conflicts:
#	iOS/Settings/Settings.storyboard
#	iOS/Settings/SettingsViewController.swift
This commit is contained in:
Stuart Breckenridge
2023-03-14 12:27:56 +08:00
20 changed files with 417 additions and 170 deletions

View File

@@ -61,6 +61,7 @@ final class AppDefaults: ObservableObject {
static let useSystemBrowser = "useSystemBrowser"
static let currentThemeName = "currentThemeName"
static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown"
static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll"
}
let isDeveloperBuild: Bool = {
@@ -266,6 +267,15 @@ final class AppDefaults: ObservableObject {
}
}
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,

View File

@@ -35,8 +35,10 @@ class MasterFeedUnreadCountView : UIView {
}
var unreadCountString: String {
return unreadCount < 1 ? "" : "\(unreadCount)"
return unreadCount < 1 ? "" : numberFormatter.string(from: NSNumber(value: unreadCount))!
}
var numberFormatter: NumberFormatter!
private var contentSizeIsValid = false
private var _contentSize = CGSize.zero
@@ -44,11 +46,21 @@ class MasterFeedUnreadCountView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .decimal
numberFormatter = formatter
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .decimal
numberFormatter = formatter
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

View File

@@ -564,6 +564,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
return
}
if tableView.window == nil {
completion?()
return
}
tableView.performBatchUpdates {
if let deletes = changes.deletes, !deletes.isEmpty {
tableView.deleteSections(IndexSet(deletes), with: .middle)

View File

@@ -31,11 +31,15 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
private lazy var dataSource = makeDataSource()
private let searchController = UISearchController(searchResultsController: nil)
private var markAsReadOnScrollWorkItem: DispatchWorkItem?
private var markAsReadOnScrollStart: Int?
private var markAsReadOnScrollEnd: Int?
private var lastVerticlePosition: CGFloat = 0
var mainControllerIdentifier = MainControllerIdentifier.masterTimeline
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 +438,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
markAsReadOnScroll()
}
// MARK: Notifications
@@ -530,10 +535,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
updateUI()
}
@objc func scrollPositionDidChange() {
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
}
// MARK: Reloading
func queueReloadAvailableCells() {
@@ -678,7 +679,7 @@ private extension MasterTimelineViewController {
func updateToolbar() {
guard firstUnreadButton != nil else { return }
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
markAllAsReadButton.isEnabled = coordinator.canMarkAllAsRead()
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
if coordinator.isRootSplitCollapsed {
@@ -695,6 +696,8 @@ private extension MasterTimelineViewController {
}
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
lastVerticlePosition = 0
if coordinator.articles.count == 0 {
tableView.rowHeight = tableView.estimatedRowHeight
} else {
@@ -723,7 +726,6 @@ private extension MasterTimelineViewController {
}
func configure(_ cell: MasterTimelineTableViewCell, article: Article, indexPath: IndexPath) {
let iconImage = iconImageFor(article)
let featuredImage = featuredImageFor(article)
@@ -748,6 +750,54 @@ private extension MasterTimelineViewController {
return nil
}
func markAsReadOnScroll() {
// Only try to mark if we are scrolling up
defer {
lastVerticlePosition = tableView.contentOffset.y
}
guard lastVerticlePosition < tableView.contentOffset.y else {
return
}
// 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,
lastVerticlePosition < tableView.contentOffset.y,
let firstVisibleIndexPath = tableView.indexPathsForVisibleRows?.first else { return }
let firstVisibleRowRect = tableView.rectForRow(at: firstVisibleIndexPath)
guard tableView.convert(firstVisibleRowRect, to: nil).origin.y < tableView.safeAreaInsets.top - 20 else { return }
// We only mark immediately after scrolling stops, not during, to prevent scroll hitching
markAsReadOnScrollWorkItem?.cancel()
markAsReadOnScrollWorkItem = DispatchWorkItem { [weak self] in
defer {
self?.markAsReadOnScrollStart = nil
self?.markAsReadOnScrollEnd = nil
}
guard let start: Int = self?.markAsReadOnScrollStart,
let end: Int = self?.markAsReadOnScrollEnd ?? self?.markAsReadOnScrollStart,
start <= end,
let self = self else {
return
}
let articles = Array(start...end)
.map({ IndexPath(row: $0, section: 0) })
.compactMap({ self.dataSource.itemIdentifier(for: $0) })
.filter({ $0.status.read == false })
self.coordinator.markAllAsRead(articles)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: markAsReadOnScrollWorkItem!)
// Here we are creating a range of rows to attempt to mark later with the work item
guard markAsReadOnScrollStart != nil else {
markAsReadOnScrollStart = max(firstVisibleIndexPath.row - 5, 0)
return
}
markAsReadOnScrollEnd = max(markAsReadOnScrollEnd ?? 0, firstVisibleIndexPath.row)
}
func toggleArticleReadStatusAction(_ article: Article) -> UIAction? {
guard !article.status.read || article.isAvailableToMarkUnread else { return nil }
@@ -875,7 +925,7 @@ private extension MasterTimelineViewController {
}
let articles = Array(fetchedArticles)
guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
guard coordinator.canMarkAllAsRead(articles), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil
}
@@ -898,7 +948,7 @@ private extension MasterTimelineViewController {
}
let articles = Array(fetchedArticles)
guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
guard coordinator.canMarkAllAsRead(articles), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 KiB

View File

@@ -1,22 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "AppIcon-1024px 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "AppIcon-1024px.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -111,7 +111,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
}
}
private var directlyMarkedAsUnreadArticles = Set<Article>()
var directlyMarkedAsUnreadArticles = Set<Article>()
var prefersStatusBarHidden = false
@@ -1043,10 +1043,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
completion?()
}
}
func canMarkAllAsRead() -> Bool {
return articles.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles)
}
func canMarkAllAsRead(_ articles: [Article]) -> Bool {
return articles.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles)
}
func canMarkAboveAsRead(for article: Article) -> Bool {
let articlesAboveArray = articles.articlesAbove(article: article)
return articlesAboveArray.canMarkAllAsRead()
return articlesAboveArray.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles)
}
func markAboveAsRead() {
@@ -1064,7 +1072,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
func canMarkBelowAsRead(for article: Article) -> Bool {
let articleBelowArray = articles.articlesBelow(article: article)
return articleBelowArray.canMarkAllAsRead()
return articleBelowArray.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles)
}
func markBelowAsRead() {

View File

@@ -7,6 +7,7 @@
//
import SwiftUI
import RSCore
struct AboutView: View, LoadableAboutData {
@@ -35,9 +36,10 @@ struct AboutView: View, LoadableAboutData {
HStack {
Spacer()
VStack(alignment: .center, spacing: 8) {
Image("About")
Image(uiImage: RSImage.appIconImage!)
.resizable()
.frame(width: 75, height: 75)
.cornerRadius(11)
Text(Bundle.main.appName)
.font(.headline)