diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 8f542410c..2db24eb0d 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -731,11 +731,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } public func fetchUnreadArticleIDs(_ completion: @escaping ArticleIDsCompletionBlock) { - database.fetchUnreadArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion) + database.fetchUnreadArticleIDsAsync(completion: completion) } public func fetchStarredArticleIDs(_ completion: @escaping ArticleIDsCompletionBlock) { - database.fetchStarredArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion) + database.fetchStarredArticleIDsAsync(completion: completion) } /// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date). diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 88ce852f6..143b90e6c 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -747,14 +747,16 @@ private extension CloudKitAccountDelegate { } case .failure(let error): + container.removeWebFeed(feed) self.refreshProgress.completeTasks(3) completion(.failure(error)) } } } else { - self.refreshProgress.completeTasks(4) - completion(.success(feed)) + self.refreshProgress.completeTasks(3) + container.removeWebFeed(feed) + completion(.failure(AccountError.createErrorNotFound)) } } diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift index 9a3046103..a2f477385 100644 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift +++ b/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift @@ -399,6 +399,7 @@ private extension TwitterFeedProvider { let decoder = JSONDecoder() let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.init(identifier: "en_US_POSIX") dateFormatter.dateFormat = Self.dateFormat decoder.dateDecodingStrategy = .formatted(dateFormatter) diff --git a/Account/Sources/Account/WebFeed.swift b/Account/Sources/Account/WebFeed.swift index d49e7a938..9fddbb5eb 100644 --- a/Account/Sources/Account/WebFeed.swift +++ b/Account/Sources/Account/WebFeed.swift @@ -77,7 +77,13 @@ public final class WebFeed: Feed, Renamable, Hashable { } } - public var name: String? + public var name: String? { + didSet { + if name != oldValue { + postDisplayNameDidChangeNotification() + } + } + } public var authors: Set? { get { diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index ddb576ba9..c68f04679 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -222,14 +222,14 @@ public final class ArticlesDatabase { // MARK: - Status - /// Fetch the articleIDs of unread articles in feeds specified by webFeedIDs. - public func fetchUnreadArticleIDsAsync(webFeedIDs: Set, completion: @escaping ArticleIDsCompletionBlock) { - articlesTable.fetchUnreadArticleIDsAsync(webFeedIDs, completion) + /// Fetch the articleIDs of unread articles. + public func fetchUnreadArticleIDsAsync(completion: @escaping ArticleIDsCompletionBlock) { + articlesTable.fetchUnreadArticleIDsAsync(completion) } - /// Fetch the articleIDs of starred articles in feeds specified by webFeedIDs. - public func fetchStarredArticleIDsAsync(webFeedIDs: Set, completion: @escaping ArticleIDsCompletionBlock) { - articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion) + /// Fetch the articleIDs of starred articles. + public func fetchStarredArticleIDsAsync(completion: @escaping ArticleIDsCompletionBlock) { + articlesTable.fetchStarredArticleIDsAsync(completion) } /// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date). diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift index c495f08aa..cdfdccf14 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -435,12 +435,12 @@ final class ArticlesTable: DatabaseTable { // MARK: - Statuses - func fetchUnreadArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { - fetchArticleIDsAsync(.read, false, webFeedIDs, completion) + func fetchUnreadArticleIDsAsync(_ completion: @escaping ArticleIDsCompletionBlock) { + statusesTable.fetchArticleIDsAsync(.read, false, completion) } - func fetchStarredArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { - fetchArticleIDsAsync(.starred, true, webFeedIDs, completion) + func fetchStarredArticleIDsAsync(_ completion: @escaping ArticleIDsCompletionBlock) { + statusesTable.fetchArticleIDsAsync(.starred, true, completion) } func fetchStarredArticleIDs() throws -> Set { @@ -785,46 +785,6 @@ private extension ArticlesTable { return articlesWithResultSet(resultSet, database) } - func fetchArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ value: Bool, _ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { - guard !webFeedIDs.isEmpty else { - completion(.success(Set())) - return - } - - queue.runInDatabase { databaseResult in - - func makeDatabaseCalls(_ database: FMDatabase) { - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - var sql = "select articleID from articles natural join statuses where feedID in \(placeholders) and \(statusKey.rawValue)=" - sql += value ? "1" : "0" - sql += ";" - - let parameters = Array(webFeedIDs) as [Any] - - guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { - DispatchQueue.main.async { - completion(.success(Set())) - } - return - } - - let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } - DispatchQueue.main.async { - completion(.success(articleIDs)) - } - } - - switch databaseResult { - case .success(let database): - makeDatabaseCalls(database) - case .failure(let databaseError): - DispatchQueue.main.async { - completion(.failure(databaseError)) - } - } - } - } - func fetchArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 if webFeedIDs.isEmpty { diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift index bf5e59d99..bb53a36a2 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift @@ -102,6 +102,38 @@ final class StatusesTable: DatabaseTable { return try fetchArticleIDs("select articleID from statuses where starred=1;") } + func fetchArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ value: Bool, _ completion: @escaping ArticleIDsCompletionBlock) { + queue.runInDatabase { databaseResult in + + func makeDatabaseCalls(_ database: FMDatabase) { + var sql = "select articleID from statuses where \(statusKey.rawValue)=" + sql += value ? "1" : "0" + sql += ";" + + guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { + DispatchQueue.main.async { + completion(.success(Set())) + } + return + } + + let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } + DispatchQueue.main.async { + completion(.success(articleIDs)) + } + } + + switch databaseResult { + case .success(let database): + makeDatabaseCalls(database) + case .failure(let databaseError): + DispatchQueue.main.async { + completion(.failure(databaseError)) + } + } + } + } + func fetchArticleIDsForStatusesWithoutArticlesNewerThan(_ cutoffDate: Date, _ completion: @escaping ArticleIDsCompletionBlock) { queue.runInDatabase { databaseResult in diff --git a/Mac/MainWindow/IconView.swift b/Mac/MainWindow/IconView.swift index edbbbace8..43e4c8444 100644 --- a/Mac/MainWindow/IconView.swift +++ b/Mac/MainWindow/IconView.swift @@ -102,6 +102,10 @@ private extension IconView { } func rectForImageView() -> NSRect { + guard !(iconImage?.isSymbol ?? false) else { + return NSMakeRect(0.0, 0.0, bounds.size.width, bounds.size.height) + } + guard let image = iconImage?.image else { return NSRect.zero } diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index bdacce4db..4db72dd1e 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -197,7 +197,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } if item.action == #selector(nextUnread(_:)) { - return canGoToNextUnread() + return canGoToNextUnread(wrappingToTop: true) } if item.action == #selector(markAllAsRead(_:)) { diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index a664ffc7a..472728e47 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -106,7 +106,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr // When the array is the same — same articles, same order — // but some data in some of the articles may have changed. // Just reload visible cells in this case: don’t call reloadData. - articleRowMap = [String: Int]() + articleRowMap = [String: [Int]]() reloadVisibleCells() return } @@ -124,7 +124,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr showFeedNames = .feed } - articleRowMap = [String: Int]() + articleRowMap = [String: [Int]]() tableView.reloadData() } } @@ -141,7 +141,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() private var exceptionArticleFetcher: ArticleFetcher? - private var articleRowMap = [String: Int]() // articleID: rowIndex + private var articleRowMap = [String: [Int]]() // articleID: rowIndex private var cellAppearance: TimelineCellAppearance! private var cellAppearanceWithIcon: TimelineCellAppearance! private var showFeedNames: TimelineShowFeedName = .none { @@ -1057,20 +1057,25 @@ private extension TimelineViewController { restoreSelection(savedSelection) } - func row(for articleID: String) -> Int? { + func rows(for articleID: String) -> [Int]? { updateArticleRowMapIfNeeded() return articleRowMap[articleID] } - func row(for article: Article) -> Int? { - return row(for: article.articleID) + func rows(for article: Article) -> [Int]? { + return rows(for: article.articleID) } func updateArticleRowMap() { - var rowMap = [String: Int]() + var rowMap = [String: [Int]]() var index = 0 articles.forEach { (article) in - rowMap[article.articleID] = index + if var indexes = rowMap[article.articleID] { + indexes.append(index) + rowMap[article.articleID] = indexes + } else { + rowMap[article.articleID] = [index] + } index += 1 } articleRowMap = rowMap @@ -1086,11 +1091,11 @@ private extension TimelineViewController { var indexes = IndexSet() articleIDs.forEach { (articleID) in - guard let oneIndex = row(for: articleID) else { + guard let rowsIndex = rows(for: articleID) else { return } - if oneIndex != NSNotFound { - indexes.insert(oneIndex) + for rowIndex in rowsIndex { + indexes.insert(rowIndex) } } diff --git a/Mac/Preferences/Accounts/AddAccountsView.swift b/Mac/Preferences/Accounts/AddAccountsView.swift index 1eefa3d4b..1032c7968 100644 --- a/Mac/Preferences/Accounts/AddAccountsView.swift +++ b/Mac/Preferences/Accounts/AddAccountsView.swift @@ -8,6 +8,7 @@ import SwiftUI import Account +import RSCore enum AddAccountSections: Int, CaseIterable { case local = 0 @@ -53,11 +54,11 @@ enum AddAccountSections: Int, CaseIterable { case .icloud: return [.cloudKit] case .web: - #if DEBUG - return [.bazQux, .feedbin, .feedly, .feedWrangler, .inoreader, .newsBlur, .theOldReader] - #else - return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader] - #endif + if AppDefaults.shared.isDeveloperBuild { + return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader].filter({ $0.isDeveloperRestricted == false }) + } else { + return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader] + } case .selfhosted: return [.freshRSS] case .allOrdered: @@ -67,12 +68,17 @@ enum AddAccountSections: Int, CaseIterable { AddAccountSections.selfhosted.sectionContent } } + + + + } struct AddAccountsView: View { weak var parent: NSHostingController? // required because presentationMode.dismiss() doesn't work var addAccountDelegate: AccountsPreferencesAddAccountDelegate? + private let chunkLimit = 4 // use this to control number of accounts in each web account column @State private var selectedAccount: AccountType = .onMyMac init(delegate: AccountsPreferencesAddAccountDelegate?) { @@ -157,7 +163,7 @@ struct AddAccountsView: View { account.image() .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 25, height: 25, alignment: .center) + .frame(width: 20, height: 20, alignment: .center) .padding(.leading, 4) Text(account.localizedAccountName()) } @@ -189,7 +195,7 @@ struct AddAccountsView: View { account.image() .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 25, height: 25, alignment: .center) + .frame(width: 20, height: 20, alignment: .center) .padding(.leading, 4) Text(account.localizedAccountName()) @@ -207,6 +213,7 @@ struct AddAccountsView: View { } } + @ViewBuilder var webAccounts: some View { VStack(alignment: .leading) { Text("Web") @@ -214,22 +221,28 @@ struct AddAccountsView: View { .padding(.horizontal) .padding(.top, 8) - Picker(selection: $selectedAccount, label: Text(""), content: { - ForEach(AddAccountSections.web.sectionContent.filter({ isRestricted($0) != true }), id: \.self, content: { account in - - HStack(alignment: .center) { - account.image() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 25, height: 25, alignment: .center) - .padding(.leading, 4) - - Text(account.localizedAccountName()) + HStack { + ForEach(0.. Bool { - if AppDefaults.shared.isDeveloperBuild && accountType.isDeveloperRestricted { - return true - } - return false + private func chunkedWebAccounts() -> [[AccountType]] { + AddAccountSections.web.sectionContent.chunked(into: chunkLimit) } + } diff --git a/Multiplatform/macOS/Article/IconView.swift b/Multiplatform/macOS/Article/IconView.swift index 6513d485d..9a72b5dd5 100644 --- a/Multiplatform/macOS/Article/IconView.swift +++ b/Multiplatform/macOS/Article/IconView.swift @@ -100,6 +100,10 @@ private extension IconView { } func rectForImageView() -> NSRect { + guard !(iconImage?.isSymbol ?? false) else { + return NSMakeRect(0.0, 0.0, bounds.size.width, bounds.size.height) + } + guard let image = iconImage?.image else { return NSRect.zero } diff --git a/Technotes/ReleaseNotes-Mac.markdown b/Technotes/ReleaseNotes-Mac.markdown index 9f8acb01d..c3abcbf58 100644 --- a/Technotes/ReleaseNotes-Mac.markdown +++ b/Technotes/ReleaseNotes-Mac.markdown @@ -1,5 +1,22 @@ # Mac Release Notes +### 6.0.1 build 6030 - 1 Apr 2021 + +Adjusted layout of the add account sheet so that it fits on smaller monitors +Sidebar: properly scale the smart feed icons when sidebar is set to large size in System Preferences + +### 6.0.1b2 build 6029 - 29 Mar 2021 + +Twitter: fixed a date parsing bug that could affect people in some locales, which would prevent Twitter feeds from working for them +Feeds list: fixed bug where newly added feed would be called Untitled past the time when the app actually knows its name +Fixed bug where next-unread command wouldn’t wrap around when you got to the bottom of the Feeds list + +### 6.0.1b1 build 6028 - 28 Mar 2021 + +Timeline: fix bug updating article display when an article with the same article ID appears more than once (which can happen when a person has multiple accounts) +iCloud: won’t add feeds that aren’t parseable, which fixes an error upon trying to rename one of these feeds +Feedbin: fixed a bug with read/unread status syncing + ### 6.0 build 6027 - 26 Mar 2021 No code changes since 6.0b5 diff --git a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig index 59d7e29e6..eb5f690ce 100644 --- a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig @@ -1,6 +1,6 @@ // High Level Settings common to both the Mac application and any extensions we bundle with it -MARKETING_VERSION = 6.0 -CURRENT_PROJECT_VERSION = 6026 +MARKETING_VERSION = 6.0.1 +CURRENT_PROJECT_VERSION = 6030 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;