From cca3089218305429f5a693306b8ef916b23afa2a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 27 Sep 2019 21:56:16 -0700 Subject: [PATCH 001/189] Update RSParser. --- submodules/RSParser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSParser b/submodules/RSParser index d3fe846f8..f01129d76 160000 --- a/submodules/RSParser +++ b/submodules/RSParser @@ -1 +1 @@ -Subproject commit d3fe846f8969f8914d233242b711f4314ec1cb17 +Subproject commit f01129d762eba20cd11a680bbde651ca75639ef3 From 07a631309cddff6adf71a9a3dffdb46f5e63bf6d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 27 Sep 2019 22:59:15 -0700 Subject: [PATCH 002/189] Update RSWeb. --- submodules/RSWeb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSWeb b/submodules/RSWeb index d140c97a1..9cb7ca961 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit d140c97a13799a38fe62c09d1719c3963c3df3ce +Subproject commit 9cb7ca96182b3320882522708a2b4dcdaafb07f6 From d7b45a14137725c9b55dfa5c66e979c3b976b931 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 27 Sep 2019 23:01:31 -0700 Subject: [PATCH 003/189] =?UTF-8?q?Change=20parseDatePublished()=20to=20a?= =?UTF-8?q?=20lazy=20var=20parsedDatePublished=20=E2=80=94=20it=20appeared?= =?UTF-8?q?=20that=20it=20was=20getting=20called=20more=20than=20once,=20a?= =?UTF-8?q?nd=20date=20parsing=20is=20expensive.=20Also:=20use=20RSDateWit?= =?UTF-8?q?hString=20rather=20than=20an=20NSDateFormatter,=20since=20NSDat?= =?UTF-8?q?eFormatter=20is=20so=20massively=20slow.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feedbin/FeedbinAccountDelegate.swift | 2 +- Frameworks/Account/Feedbin/FeedbinEntry.swift | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index f7b801b8c..bbd185238 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -1109,7 +1109,7 @@ private extension FeedbinAccountDelegate { let parsedItems: [ParsedItem] = entries.map { entry in let authors = Set([ParsedAuthor(name: entry.authorName, url: entry.jsonFeed?.jsonFeedAuthor?.url, avatarURL: entry.jsonFeed?.jsonFeedAuthor?.avatarURL, emailAddress: nil)]) - return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: String(entry.feedID), url: nil, externalURL: entry.url, title: entry.title, contentHTML: entry.contentHTML, contentText: nil, summary: entry.summary, imageURL: nil, bannerImageURL: nil, datePublished: entry.parseDatePublished(), dateModified: nil, authors: authors, tags: nil, attachments: nil) + return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: String(entry.feedID), url: nil, externalURL: entry.url, title: entry.title, contentHTML: entry.contentHTML, contentText: nil, summary: entry.summary, imageURL: nil, bannerImageURL: nil, datePublished: entry.parsedDatePublished, dateModified: nil, authors: authors, tags: nil, attachments: nil) } return Set(parsedItems) diff --git a/Frameworks/Account/Feedbin/FeedbinEntry.swift b/Frameworks/Account/Feedbin/FeedbinEntry.swift index 8d73a51da..82b40e186 100644 --- a/Frameworks/Account/Feedbin/FeedbinEntry.swift +++ b/Frameworks/Account/Feedbin/FeedbinEntry.swift @@ -10,7 +10,7 @@ import Foundation import RSParser import RSCore -struct FeedbinEntry: Codable { +final class FeedbinEntry: Codable { let articleID: Int let feedID: Int @@ -23,6 +23,19 @@ struct FeedbinEntry: Codable { let dateArrived: String? let jsonFeed: FeedbinEntryJSONFeed? + // Feedbin dates can't be decoded by the JSONDecoding 8601 decoding strategy. Feedbin + // requires a very specific date formatter to work and even then it fails occasionally. + // Rather than loose all the entries we only lose the one date by decoding as a string + // and letting the one date fail when parsed. + lazy var parsedDatePublished: Date? = { + if let datePublished = datePublished { + return RSDateWithString(datePublished) + } + else { + return nil + } + }() + enum CodingKeys: String, CodingKey { case articleID = "id" case feedID = "feed_id" @@ -35,19 +48,6 @@ struct FeedbinEntry: Codable { case dateArrived = "created_at" case jsonFeed = "json_feed" } - - // Feedbin dates can't be decoded by the JSONDecoding 8601 decoding strategy. Feedbin - // requires a very specific date formatter to work and even then it fails occasionally. - // Rather than loose all the entries we only lose the one date by decoding as a string - // and letting the one date fail when parsed. - func parseDatePublished() -> Date? { - if datePublished != nil { - return FeedbinDate.formatter.date(from: datePublished!) - } else { - return nil - } - } - } struct FeedbinEntryJSONFeed: Codable { From 35240eb2cfe6218084d1627979ec935e192efe31 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 27 Sep 2019 23:02:14 -0700 Subject: [PATCH 004/189] Update RSParser and RSWeb. --- submodules/RSParser | 2 +- submodules/RSWeb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/RSParser b/submodules/RSParser index d3fe846f8..f01129d76 160000 --- a/submodules/RSParser +++ b/submodules/RSParser @@ -1 +1 @@ -Subproject commit d3fe846f8969f8914d233242b711f4314ec1cb17 +Subproject commit f01129d762eba20cd11a680bbde651ca75639ef3 diff --git a/submodules/RSWeb b/submodules/RSWeb index d140c97a1..9cb7ca961 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit d140c97a13799a38fe62c09d1719c3963c3df3ce +Subproject commit 9cb7ca96182b3320882522708a2b4dcdaafb07f6 From 7c269214874c8214763345fd5a4f7ca975a0ab81 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 28 Sep 2019 06:20:06 -0500 Subject: [PATCH 005/189] Change Feed Inspector Reader View setting layout --- Mac/Inspector/Inspector.storyboard | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Mac/Inspector/Inspector.storyboard b/Mac/Inspector/Inspector.storyboard index ed2237926..4f5f22bf5 100644 --- a/Mac/Inspector/Inspector.storyboard +++ b/Mac/Inspector/Inspector.storyboard @@ -1,8 +1,8 @@ - + - + @@ -63,7 +63,7 @@ Field - + @@ -71,7 +71,7 @@ Field - + @@ -79,7 +79,7 @@ Field - + @@ -87,7 +87,7 @@ Field - + @@ -95,8 +95,8 @@ Field + - - + @@ -122,8 +122,8 @@ Field - + From f785e9f839f71cd325d2e20c9a1d356c670688f5 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 28 Sep 2019 06:35:21 -0500 Subject: [PATCH 006/189] Update the feed icon if it wasn't available when the inspector was first shown --- iOS/Inspector/FeedInspectorView.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift index 07b1c2a7a..66d15d2f9 100644 --- a/iOS/Inspector/FeedInspectorView.swift +++ b/iOS/Inspector/FeedInspectorView.swift @@ -30,7 +30,7 @@ struct FeedInspectorView : View { }) { TextField("Feed Name", text: $viewModel.name) Toggle(isOn: $viewModel.isArticleExtractorAlwaysOn) { - Text("Reader View is always on") + Text("Always Show Reader View") } } Section(header: Text("HOME PAGE")) { @@ -54,6 +54,7 @@ struct FeedInspectorView : View { init(feed: Feed) { self.feed = feed + NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil) } var image: UIImage { @@ -97,7 +98,11 @@ struct FeedInspectorView : View { var feedLinkURL: String { return feed.url } - + + @objc func feedIconDidBecomeAvailable(_ notification: Notification) { + objectWillChange.send() + } + } } From 09a3a03fc47757ea041d58ece6d576f600d1ad00 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 28 Sep 2019 07:00:18 -0500 Subject: [PATCH 007/189] Add Get Info context menu option --- iOS/AppAssets.swift | 4 +++ iOS/MasterFeed/MasterFeedViewController.swift | 33 +++++++++++++++++++ iOS/SceneCoordinator.swift | 6 ++++ 3 files changed, 43 insertions(+) diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 1ad368e43..a31ac6ea5 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -68,6 +68,10 @@ struct AppAssets { return RSImage(named: "faviconTemplateImage")! }() + static var infoImage: UIImage = { + UIImage(systemName: "info.circle")! + }() + static var markAllInFeedAsReadImage: UIImage = { return UIImage(systemName: "asterisk.circle")! }() diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index a0681824a..f6e0c823b 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -229,6 +229,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1) } + if let action = self.getInfoAlertAction(indexPath: indexPath, completionHandler: completionHandler) { + alert.addAction(action) + } + if let action = self.homePageAlertAction(indexPath: indexPath, completionHandler: completionHandler) { alert.addAction(action) } @@ -697,6 +701,10 @@ private extension MasterFeedViewController { var actions = [UIAction]() + if let inspectorAction = self.getInfoAction(indexPath: indexPath) { + actions.append(inspectorAction) + } + if let homePageAction = self.homePageAction(indexPath: indexPath) { actions.append(homePageAction) } @@ -835,6 +843,31 @@ private extension MasterFeedViewController { return action } + func getInfoAction(indexPath: IndexPath) -> UIAction? { + guard let node = dataSource.itemIdentifier(for: indexPath), let feed = node.representedObject as? Feed else { + return nil + } + + let title = NSLocalizedString("Get Info", comment: "Get Info") + let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in + self?.coordinator.showFeedInspector(for: feed) + } + return action + } + + func getInfoAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? { + guard let node = dataSource.itemIdentifier(for: indexPath), let feed = node.representedObject as? Feed else { + return nil + } + + let title = NSLocalizedString("Get Info", comment: "Get Infor") + let action = UIAlertAction(title: title, style: .default) { [weak self] action in + self?.coordinator.showFeedInspector(for: feed) + completionHandler(true) + } + return action + } + func rename(indexPath: IndexPath) { let name = (dataSource.itemIdentifier(for: indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? "" diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index bfd9c45fd..9a845871d 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -800,6 +800,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } } + func showFeedInspector(for feed: Feed) { + rootSplitViewController.present(style: .formSheet) { + FeedInspectorView(viewModel: FeedInspectorView.ViewModel(feed: feed)) + } + } + func showAdd(_ type: AddControllerType, initialFeed: String? = nil, initialFeedName: String? = nil) { selectFeed(nil) From a37e4b79207189a31eaa3d14e68fdac81469cff3 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 28 Sep 2019 12:11:33 -0500 Subject: [PATCH 008/189] Fixed feed separators for disclosure rows --- iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift index 1a89285bb..2df98fe02 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift @@ -13,7 +13,7 @@ struct MasterFeedTableViewCellLayout { private static let editingControlIndent = CGFloat(integerLiteral: 40) private static let imageSize = CGSize(width: 20, height: 20) - private static let imageMarginRight = CGFloat(integerLiteral: 8) + private static let imageMarginRight = CGFloat(integerLiteral: 11) private static let unreadCountMarginLeft = CGFloat(integerLiteral: 8) private static let unreadCountMarginRight = CGFloat(integerLiteral: 16) private static let disclosureButtonSize = CGSize(width: 44, height: 44) @@ -58,7 +58,11 @@ struct MasterFeedTableViewCellLayout { } // Separator Insets - separatorInsets = UIEdgeInsets(top: 0, left: rFavicon.maxX + MasterFeedTableViewCellLayout.imageMarginRight, bottom: 0, right: 0) + if shouldShowDisclosure { + separatorInsets = UIEdgeInsets(top: 0, left: MasterFeedTableViewCellLayout.disclosureButtonSize.width, bottom: 0, right: 0) + } else { + separatorInsets = UIEdgeInsets(top: 0, left: rFavicon.maxX + MasterFeedTableViewCellLayout.imageMarginRight, bottom: 0, right: 0) + } // Unread Count let unreadCountSize = unreadCountView.contentSize From 4e78305a85efea4ac94f2459220da18186a0160f Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 28 Sep 2019 11:31:39 -0700 Subject: [PATCH 009/189] Update Slack group link to point to link that redirects to the new Slack invitation page. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f9cab550..020eb826d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Here’s [How to Support NetNewsWire](Technotes/HowToSupportNetNewsWire.markdown #### Community -[Join the Slack group](https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc) to talk with other NetNewsWire users — and to help out, if you’d like to, by testing, coding, writing, providing feedback, or just helping us think things through. Everybody is welcome and encouraged to join. +[Join the Slack group](https://ranchero.com/netnewswire/slack) to talk with other NetNewsWire users — and to help out, if you’d like to, by testing, coding, writing, providing feedback, or just helping us think things through. Everybody is welcome and encouraged to join. Every community member is expected to abide by the code of conduct which is included in the [Contributing](CONTRIBUTING.md) page. From 2b491217f3fbb33b3d03a962bc58eaf3c8f66881 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 28 Sep 2019 12:18:08 -0700 Subject: [PATCH 010/189] =?UTF-8?q?Create=20statusWithRow(=5F=20row:=20FMR?= =?UTF-8?q?esultSet,=20articleID:=20String)=20=E2=80=94=C2=A0it=20allows?= =?UTF-8?q?=20us=20to=20avoid=20pulling=20articleID=20from=20the=20row=20t?= =?UTF-8?q?wice=20every=20time=20we=E2=80=99re=20creating=20a=20DatabaseAr?= =?UTF-8?q?ticle.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frameworks/ArticlesDatabase/ArticlesTable.swift | 13 ++++++------- Frameworks/ArticlesDatabase/StatusesTable.swift | 8 ++++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 3ac4de861..a92354045 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -475,16 +475,15 @@ private extension ArticlesTable { func makeDatabaseArticles(with resultSet: FMResultSet) -> Set { let articles = resultSet.mapToSet { (row) -> DatabaseArticle? in - // The resultSet is a result of a JOIN query with the statuses table, - // so we can get the statuses at the same time and avoid additional database lookups. - - guard let status = statusesTable.statusWithRow(resultSet) else { - assertionFailure("Expected status.") + guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { + assertionFailure("Expected articleID.") return nil } - guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { - assertionFailure("Expected articleID.") + // The resultSet is a result of a JOIN query with the statuses table, + // so we can get the statuses at the same time and avoid additional database lookups. + guard let status = statusesTable.statusWithRow(resultSet, articleID: articleID) else { + assertionFailure("Expected status.") return nil } guard let feedID = row.string(forColumn: DatabaseKey.feedID) else { diff --git a/Frameworks/ArticlesDatabase/StatusesTable.swift b/Frameworks/ArticlesDatabase/StatusesTable.swift index 0bb18377d..fa5a741ab 100644 --- a/Frameworks/ArticlesDatabase/StatusesTable.swift +++ b/Frameworks/ArticlesDatabase/StatusesTable.swift @@ -105,17 +105,21 @@ final class StatusesTable: DatabaseTable { guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { return nil } + return statusWithRow(row, articleID: articleID) + } + + func statusWithRow(_ row: FMResultSet, articleID: String) ->ArticleStatus? { if let cachedStatus = cache[articleID] { return cachedStatus } - + guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else { return nil } let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row) cache.addStatusIfNotCached(articleStatus) - + return articleStatus } From 37c9818cad8511b9c507bbdafdca87a7e1bcafeb Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 28 Sep 2019 13:51:33 -0700 Subject: [PATCH 011/189] =?UTF-8?q?Create=20and=20use=20a=20cache=20for=20?= =?UTF-8?q?DatabaseArticle=20=E2=80=94=20this=20will=20make=20fetches=20fa?= =?UTF-8?q?ster,=20since=20we=20can=20skip=20pulling=20the=20same=20data?= =?UTF-8?q?=20from=20the=20database=20over=20and=20over.=20Articles=20in?= =?UTF-8?q?=20the=20cache=20are=20removed=20when=20articles=20are=20update?= =?UTF-8?q?d,=20so=20the=20cache=20is=20never=20stale.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ArticlesDatabase/ArticlesTable.swift | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index a92354045..8f06afdc8 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -20,6 +20,7 @@ final class ArticlesTable: DatabaseTable { private let statusesTable: StatusesTable private let authorsLookupTable: DatabaseLookupTable private let attachmentsLookupTable: DatabaseLookupTable + private var databaseArticlesCache = [String: DatabaseArticle]() private lazy var searchTable: SearchTable = { return SearchTable(queue: queue, articlesTable: self) @@ -480,6 +481,12 @@ private extension ArticlesTable { return nil } + // Articles are removed from the cache when they’re updated. + // See saveUpdatedArticles. + if let databaseArticle = databaseArticlesCache[articleID] { + return databaseArticle + } + // The resultSet is a result of a JOIN query with the statuses table, // so we can get the statuses at the same time and avoid additional database lookups. guard let status = statusesTable.statusWithRow(resultSet, articleID: articleID) else { @@ -506,7 +513,9 @@ private extension ArticlesTable { let datePublished = row.date(forColumn: DatabaseKey.datePublished) let dateModified = row.date(forColumn: DatabaseKey.dateModified) - return DatabaseArticle(articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, status: status) + let databaseArticle = DatabaseArticle(articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, status: status) + databaseArticlesCache[articleID] = databaseArticle + return databaseArticle } return articles @@ -662,6 +671,7 @@ private extension ArticlesTable { func saveUpdatedArticles(_ updatedArticles: Set
, _ fetchedArticles: [String: Article], _ database: FMDatabase) { + removeArticlesFromDatabaseArticlesCache(updatedArticles) saveUpdatedRelatedObjects(updatedArticles, fetchedArticles, database) for updatedArticle in updatedArticles { @@ -682,10 +692,17 @@ private extension ArticlesTable { // Not unexpected. There may be no changes. return } - + updateRowsWithDictionary(changesDictionary, whereKey: DatabaseKey.articleID, matches: updatedArticle.articleID, database: database) } - + + func removeArticlesFromDatabaseArticlesCache(_ updatedArticles: Set
) { + let articleIDs = updatedArticles.articleIDs() + for articleID in articleIDs { + databaseArticlesCache[articleID] = nil + } + } + func statusIndicatesArticleIsIgnorable(_ status: ArticleStatus) -> Bool { // Ignorable articles: either userDeleted==1 or (not starred and arrival date > 4 months). if status.userDeleted { From c51f5f44e040ecbbd762c6a277d6d91372084f1e Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 29 Sep 2019 14:07:33 -0500 Subject: [PATCH 012/189] Add eclipses to truncated timeline text. Issue #1072 --- iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift index 025c4ade9..b512da4f8 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift @@ -105,7 +105,7 @@ private extension MasterTimelineTableViewCell { static func multiLineUILabel() -> UILabel { let label = NonIntrinsicLabel() label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping + label.lineBreakMode = .byTruncatingTail label.allowsDefaultTighteningForTruncation = false label.adjustsFontForContentSizeCategory = true return label From 1f26a91af9526391990379d148a2a1faf302d324 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 29 Sep 2019 15:53:50 -0500 Subject: [PATCH 013/189] Prevent scrolling while reloading cells Issue #1085 --- .../MasterTimelineViewController.swift | 12 ++++++++---- iOS/SceneCoordinator.swift | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 78f5e919b..a74a96021 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -137,9 +137,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner // MARK: API - func restoreSelectionIfNecessary() { + func restoreSelectionIfNecessary(adjustScroll: Bool) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed) + if adjustScroll { + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed) + } else { + tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) + } } } @@ -390,7 +394,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner var snapshot = dataSource.snapshot() snapshot.reloadItems(articles) dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in - self?.restoreSelectionIfNecessary() + self?.restoreSelectionIfNecessary(adjustScroll: false) } } @@ -507,7 +511,7 @@ private extension MasterTimelineViewController { snapshot.appendItems(coordinator.articles, toSection: 0) dataSource.apply(snapshot, animatingDifferences: animate) { [weak self] in - self?.restoreSelectionIfNecessary() + self?.restoreSelectionIfNecessary(adjustScroll: false) completion?() } } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 9a845871d..1a54b158a 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1516,7 +1516,7 @@ private extension SceneCoordinator { subSplitViewController!.showDetailViewController(navController, sender: self) masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true) - masterTimelineViewController!.restoreSelectionIfNecessary() + masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: true) // We made sure this was there above when we called configureDoubleSplit return subSplitViewController! From 0f362b7680727418ba24f04d3e1b204e60f248da Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 29 Sep 2019 17:01:20 -0700 Subject: [PATCH 014/189] Update RSDatabase. --- submodules/RSDatabase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 9bb7e3745..807bb445f 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 9bb7e374518ad481c6da14b656d6676c02b33548 +Subproject commit 807bb445f0bc692cb6b9210f66db82a85efe5c59 From 3ffd461448236e0f198a013f0d887d8f586e99be Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 29 Sep 2019 17:02:10 -0700 Subject: [PATCH 015/189] Update RSDatabase. --- submodules/RSDatabase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 807bb445f..9bb7e3745 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 807bb445f0bc692cb6b9210f66db82a85efe5c59 +Subproject commit 9bb7e374518ad481c6da14b656d6676c02b33548 From 6a9435a63ee3c2dfd67a76d9f9baeea59129b6cd Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 29 Sep 2019 17:03:00 -0700 Subject: [PATCH 016/189] Update RSDatabase. --- submodules/RSDatabase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 9bb7e3745..807bb445f 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 9bb7e374518ad481c6da14b656d6676c02b33548 +Subproject commit 807bb445f0bc692cb6b9210f66db82a85efe5c59 From 9f126bfb8ff42b3bc8ce16f5a1373a3c8ce91964 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 29 Sep 2019 19:40:12 -0500 Subject: [PATCH 017/189] Add a background if the image is too dark and we are in dark mode. --- NetNewsWire.xcodeproj/project.pbxproj | 4 ++ .../Cell/MasterFeedTableViewCell.swift | 15 ++++++ iOS/UIKit Extensions/UIImage-Extensions.swift | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 iOS/UIKit Extensions/UIImage-Extensions.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 8bdb83bab..f3789f111 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -216,6 +216,7 @@ 51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; }; 51FA73AB2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; }; 51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */; }; + 51FD40C72341555A00880194 /* UIImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD40BD2341555600880194 /* UIImage-Extensions.swift */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; }; @@ -895,6 +896,7 @@ 51FA73A62332BE880090D516 /* ExtractedArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtractedArticle.swift; sourceTree = ""; }; 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorConfig.swift; sourceTree = ""; }; 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; + 51FD40BD2341555600880194 /* UIImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage-Extensions.swift"; sourceTree = ""; }; 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = ""; }; 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; @@ -1332,6 +1334,7 @@ 51934CC1230F5963006127BE /* ThemedNavigationController.swift */, 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */, 51F85BF622749FA100C787DC /* UIFont-Extensions.swift */, + 51FD40BD2341555600880194 /* UIImage-Extensions.swift */, 512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */, 51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */, ); @@ -2807,6 +2810,7 @@ 51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, 5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */, + 51FD40C72341555A00880194 /* UIImage-Extensions.swift in Sources */, 5132293B23305D4C0033D4ED /* SettingsAboutView.swift in Sources */, 84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift index 1322c8b72..25d481c2d 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift @@ -34,6 +34,21 @@ class MasterFeedTableViewCell : NNWTableViewCell { var faviconImage: UIImage? { didSet { faviconImageView.image = faviconImage + + if self.traitCollection.userInterfaceStyle == .dark { + DispatchQueue.global(qos: .background).async { + if self.faviconImage?.isDark() ?? false { + DispatchQueue.main.async { + self.faviconImageView.backgroundColor = AppAssets.avatarBackgroundColor + } + } else { + DispatchQueue.main.async { + self.faviconImageView.backgroundColor = nil + } + } + } + } + } } diff --git a/iOS/UIKit Extensions/UIImage-Extensions.swift b/iOS/UIKit Extensions/UIImage-Extensions.swift new file mode 100644 index 000000000..a64d3965d --- /dev/null +++ b/iOS/UIKit Extensions/UIImage-Extensions.swift @@ -0,0 +1,47 @@ +// +// UIImage-Extensions.swift +// NetNewsWire +// +// Created by Maurice Parker on 9/29/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +extension CGImage { + + func isDark() -> Bool { + guard let imageData = self.dataProvider?.data else { return false } + guard let ptr = CFDataGetBytePtr(imageData) else { return false } + + let length = CFDataGetLength(imageData) + var visiblePixels = 0 + var darkPixels = 0 + + for i in stride(from: 0, to: length, by: 4) { + + let r = ptr[i] + let g = ptr[i + 1] + let b = ptr[i + 2] + let a = ptr[i + 3] + let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) + + if Double(a) > 0.0 { + visiblePixels += 1 + if luminance < 50 { + darkPixels += 1 + } + } + + } + + return Double(darkPixels) / Double(visiblePixels) > 0.4 + } + +} + +extension UIImage { + func isDark() -> Bool { + return self.cgImage?.isDark() ?? false + } +} From b62a7afa10c828c84e1a2900399eb49a1b03d8fa Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 29 Sep 2019 22:34:31 -0700 Subject: [PATCH 018/189] =?UTF-8?q?Add=20keyboard=20shortcut=20for=20toggl?= =?UTF-8?q?ing=20starred=20status=20=E2=80=94=C2=A0s=20key.=20Fix=20#875.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist b/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist index 3f715052d..6218cd47e 100644 --- a/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist +++ b/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist @@ -108,5 +108,11 @@ action goToNextSubscription: + + key + s + action + toggleStarred: + From 27478b0f6f4c18caec206b7f53051a926145f7c2 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 29 Sep 2019 22:44:32 -0700 Subject: [PATCH 019/189] Make r and u both just toggle read status. Update keyboard shortcuts HTML documentation to match. --- Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist | 4 ++-- Mac/Resources/KeyboardShortcuts/KeyboardShortcuts.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist b/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist index 6218cd47e..32695e230 100644 --- a/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist +++ b/Mac/MainWindow/Keyboard/GlobalKeyboardShortcuts.plist @@ -46,7 +46,7 @@ key r action - markRead: + toggleRead: key @@ -76,7 +76,7 @@ key u action - markUnread: + toggleRead: key diff --git a/Mac/Resources/KeyboardShortcuts/KeyboardShortcuts.html b/Mac/Resources/KeyboardShortcuts/KeyboardShortcuts.html index 92c64a5ee..d44dd4880 100644 --- a/Mac/Resources/KeyboardShortcuts/KeyboardShortcuts.html +++ b/Mac/Resources/KeyboardShortcuts/KeyboardShortcuts.html @@ -51,12 +51,12 @@ Scroll or go to next unreadspace Go to next unreadn or + - Mark as readr + Toggle read statusr or u Mark all as readk Mark older articles as reado Mark all as read, go to next unreadl Mark as unread, go to next unreadm - Mark as unreadu + Toggle starred statuss Open in browserb or or Enter Previous subscriptiona Next subscriptionz From 098128fd0af60864bbff1e8f4bfb34390c5b644f Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 29 Sep 2019 22:46:40 -0700 Subject: [PATCH 020/189] Use new Slack group URL with Help menu command. Fix #1087. --- Mac/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index eb122e939..581126f56 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -454,7 +454,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func openSlackGroup(_ sender: Any?) { - Browser.open("https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc", inBackground: false) + Browser.open("https://ranchero.com/netnewswire/slack", inBackground: false) } @IBAction func openTechnotes(_ sender: Any?) { From 8f53916a79695d39950d4f09e5a9400fd65f5d46 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Mon, 30 Sep 2019 09:45:13 +1000 Subject: [PATCH 021/189] Make Account framework tests compile and pass. --- .../AccountTests/AccountCredentialsTest.swift | 34 +++++++++++-------- .../AccountTests/AccountFeedSyncTest.swift | 4 +-- .../AccountFolderContentsSyncTest.swift | 6 ++-- .../AccountTests/AccountFolderSyncTest.swift | 6 ++-- .../Account/AccountTests/TestTransport.swift | 23 ++++++++++--- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift index d9e7ba91f..2d1f2a85d 100644 --- a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift +++ b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift @@ -26,12 +26,12 @@ class AccountCredentialsTest: XCTestCase { // Make sure any left over from failed tests are gone do { - try account.removeBasicCredentials() + try account.removeCredentials(type: .basic) } catch { XCTFail(error.localizedDescription) } - var credentials: Credentials? = Credentials.basic(username: "maurice", password: "hardpasswd") + var credentials: Credentials? = Credentials(type: .basic, username: "maurice", secret: "hardpasswd") // Store the credentials do { @@ -43,19 +43,21 @@ class AccountCredentialsTest: XCTestCase { // Retrieve them credentials = nil do { - credentials = try account.retrieveBasicCredentials() + credentials = try account.retrieveCredentials(type: .basic) } catch { XCTFail(error.localizedDescription) } - switch credentials! { - case .basic(let username, let password): - XCTAssertEqual("maurice", username) - XCTAssertEqual("hardpasswd", password) + switch credentials!.type { + case .basic: + XCTAssertEqual("maurice", credentials?.username) + XCTAssertEqual("hardpasswd", credentials?.secret) + default: + XCTFail("Expected \(CredentialsType.basic), received \(credentials!.type)") } // Update them - credentials = Credentials.basic(username: "maurice", password: "easypasswd") + credentials = Credentials(type: .basic, username: "maurice", secret: "easypasswd") do { try account.storeCredentials(credentials!) } catch { @@ -65,27 +67,29 @@ class AccountCredentialsTest: XCTestCase { // Retrieve them again credentials = nil do { - credentials = try account.retrieveBasicCredentials() + credentials = try account.retrieveCredentials(type: .basic) } catch { XCTFail(error.localizedDescription) } - switch credentials! { - case .basic(let username, let password): - XCTAssertEqual("maurice", username) - XCTAssertEqual("easypasswd", password) + switch credentials!.type { + case .basic: + XCTAssertEqual("maurice", credentials?.username) + XCTAssertEqual("easypasswd", credentials?.secret) + default: + XCTFail("Expected \(CredentialsType.basic), received \(credentials!.type)") } // Delete them do { - try account.removeBasicCredentials() + try account.removeCredentials(type: .basic) } catch { XCTFail(error.localizedDescription) } // Make sure they are gone do { - try credentials = account.retrieveBasicCredentials() + try credentials = account.retrieveCredentials(type: .basic) } catch { XCTFail(error.localizedDescription) } diff --git a/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift b/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift index 07f42ace5..45131c3dd 100644 --- a/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift +++ b/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift @@ -27,7 +27,7 @@ class AccountFeedSyncTest: XCTestCase { // Test initial folders let initialExpection = self.expectation(description: "Initial feeds") - account.refreshAll() { + account.refreshAll() { _ in initialExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) @@ -44,7 +44,7 @@ class AccountFeedSyncTest: XCTestCase { testTransport.testFiles["https://api.feedbin.com/v2/subscriptions.json"] = "subscriptions_add.json" let addExpection = self.expectation(description: "Add feeds") - account.refreshAll() { + account.refreshAll() { _ in addExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) diff --git a/Frameworks/Account/AccountTests/AccountFolderContentsSyncTest.swift b/Frameworks/Account/AccountTests/AccountFolderContentsSyncTest.swift index 0be0ee639..c3ca12b3c 100644 --- a/Frameworks/Account/AccountTests/AccountFolderContentsSyncTest.swift +++ b/Frameworks/Account/AccountTests/AccountFolderContentsSyncTest.swift @@ -28,7 +28,7 @@ class AccountFolderContentsSyncTest: XCTestCase { // Test initial folders let initialExpection = self.expectation(description: "Initial contents") - account.refreshAll() { + account.refreshAll() { _ in initialExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) @@ -41,7 +41,7 @@ class AccountFolderContentsSyncTest: XCTestCase { testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_add.json" let addExpection = self.expectation(description: "Add contents") - account.refreshAll() { + account.refreshAll() { _ in addExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) @@ -53,7 +53,7 @@ class AccountFolderContentsSyncTest: XCTestCase { testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_delete.json" let deleteExpection = self.expectation(description: "Delete contents") - account.refreshAll() { + account.refreshAll() { _ in deleteExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) diff --git a/Frameworks/Account/AccountTests/AccountFolderSyncTest.swift b/Frameworks/Account/AccountTests/AccountFolderSyncTest.swift index e4d4c6cb8..09831a937 100644 --- a/Frameworks/Account/AccountTests/AccountFolderSyncTest.swift +++ b/Frameworks/Account/AccountTests/AccountFolderSyncTest.swift @@ -25,7 +25,7 @@ class AccountFolderSyncTest: XCTestCase { // Test initial folders let initialExpection = self.expectation(description: "Initial tags") - account.refreshAll() { + account.refreshAll() { _ in initialExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) @@ -43,7 +43,7 @@ class AccountFolderSyncTest: XCTestCase { testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_delete.json" let deleteExpection = self.expectation(description: "Delete tags") - account.refreshAll() { + account.refreshAll() { _ in deleteExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) @@ -62,7 +62,7 @@ class AccountFolderSyncTest: XCTestCase { testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_add.json" let addExpection = self.expectation(description: "Add tags") - account.refreshAll() { + account.refreshAll() { _ in addExpection.fulfill() } waitForExpectations(timeout: 5, handler: nil) diff --git a/Frameworks/Account/AccountTests/TestTransport.swift b/Frameworks/Account/AccountTests/TestTransport.swift index 781edc98b..1884c6256 100644 --- a/Frameworks/Account/AccountTests/TestTransport.swift +++ b/Frameworks/Account/AccountTests/TestTransport.swift @@ -16,30 +16,43 @@ final class TestTransport: Transport { } var testFiles = [String: String]() + var testStatusCodes = [String: Int]() - func send(request: URLRequest, completion: @escaping (Result<(HTTPHeaders, Data?), Error>) -> Void) { + private func httpResponse(for request: URLRequest, statusCode: Int = 200) -> HTTPURLResponse { + guard let url = request.url else { + fatalError("Attempting to mock a http response for a request without a URL \(request).") + } + return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: nil)! + } + + func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) { guard let urlString = request.url?.absoluteString else { completion(.failure(TestTransportError.invalidState)) return } + let response = httpResponse(for: request, statusCode: testStatusCodes[urlString] ?? 200) + if let testFileName = testFiles[urlString] { let testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testFileName) let data = try! Data(contentsOf: testFileURL) DispatchQueue.global(qos: .background).async { - completion(.success((HTTPHeaders(), data))) + completion(.success((response, data))) } } else { DispatchQueue.global(qos: .background).async { - completion(.success((HTTPHeaders(), nil))) + completion(.success((response, nil))) } } } - func send(request: URLRequest, payload: Data, completion: @escaping (Result<(HTTPHeaders, Data?), Error>) -> Void) { - + func send(request: URLRequest, method: String, completion: @escaping (Result) -> Void) { + fatalError("Unimplemented.") } + func send(request: URLRequest, method: String, payload: Data, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) { + fatalError("Unimplemented.") + } } From 8d0bfd9b47bdd5a8d87d216dcab5c619dba97101 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 02:45:33 -0500 Subject: [PATCH 022/189] Make inspector add a background for small dark images --- iOS/Inspector/FeedInspectorView.swift | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift index 66d15d2f9..1b695a0bf 100644 --- a/iOS/Inspector/FeedInspectorView.swift +++ b/iOS/Inspector/FeedInspectorView.swift @@ -13,6 +13,7 @@ import Account struct FeedInspectorView : View { @ObservedObject var viewModel: ViewModel + @Environment(\.colorScheme) private var colorScheme: ColorScheme @Environment(\.viewController) private var viewController: UIViewController? var body: some View { @@ -22,9 +23,24 @@ struct FeedInspectorView : View { HStack { Spacer() if self.viewModel.image.size.width < 32 || self.viewModel.image.size.height < 32 { - Image(uiImage: self.viewModel.image).resizable().frame(width: 24.0, height: 24.0).cornerRadius(2.0) + if colorScheme == .dark && self.viewModel.image.isDark() { + ZStack { + Color(AppAssets.avatarBackgroundColor) + Image(uiImage: self.viewModel.image).resizable() + } + .frame(width: 24.0, height: 24.0) + .cornerRadius(2.0) + } else { + Image(uiImage: self.viewModel.image) + .resizable() + .frame(width: 24.0, height: 24.0) + .cornerRadius(2.0) + } } else { - Image(uiImage: self.viewModel.image).resizable().frame(width: 48.0, height: 48.0).cornerRadius(5.0) + Image(uiImage: self.viewModel.image) + .resizable() + .frame(width: 48.0, height: 48.0) + .cornerRadius(5.0) } Spacer() }) { From 37418471bc3f25fc709a8afdbc6602f02156c89e Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 02:48:51 -0500 Subject: [PATCH 023/189] Give large images a background in darkmode when they are dark --- iOS/Inspector/FeedInspectorView.swift | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift index 1b695a0bf..79b22175a 100644 --- a/iOS/Inspector/FeedInspectorView.swift +++ b/iOS/Inspector/FeedInspectorView.swift @@ -24,10 +24,9 @@ struct FeedInspectorView : View { Spacer() if self.viewModel.image.size.width < 32 || self.viewModel.image.size.height < 32 { if colorScheme == .dark && self.viewModel.image.isDark() { - ZStack { - Color(AppAssets.avatarBackgroundColor) - Image(uiImage: self.viewModel.image).resizable() - } + Image(uiImage: self.viewModel.image) + .resizable() + .background(Color(AppAssets.avatarBackgroundColor)) .frame(width: 24.0, height: 24.0) .cornerRadius(2.0) } else { @@ -37,10 +36,18 @@ struct FeedInspectorView : View { .cornerRadius(2.0) } } else { - Image(uiImage: self.viewModel.image) - .resizable() - .frame(width: 48.0, height: 48.0) - .cornerRadius(5.0) + if colorScheme == .dark && self.viewModel.image.isDark() { + Image(uiImage: self.viewModel.image) + .resizable() + .background(Color(AppAssets.avatarBackgroundColor)) + .frame(width: 48.0, height: 48.0) + .cornerRadius(5.0) + } else { + Image(uiImage: self.viewModel.image) + .resizable() + .frame(width: 48.0, height: 48.0) + .cornerRadius(5.0) + } } Spacer() }) { From bacff92434aa21f076f589e15505b682409ad7a7 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 02:54:19 -0500 Subject: [PATCH 024/189] Make timeline favicon have a background in darkmode when the favicon is too dark --- iOS/MasterTimeline/MasterTimelineViewController.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index a74a96021..aac438b09 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -472,7 +472,12 @@ private extension MasterTimelineViewController { if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView { self.titleView = titleView + titleView.imageView.image = coordinator.timelineFavicon + if traitCollection.userInterfaceStyle == .dark && titleView.imageView.image?.isDark() ?? false { + titleView.imageView.backgroundColor = AppAssets.avatarBackgroundColor + } + titleView.label.text = coordinator.timelineName if coordinator.timelineFetcher is Feed { From 5717c84067a5a1fa0323a3a83910355ce0f243d5 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 02:59:27 -0500 Subject: [PATCH 025/189] Make generated favicons a little bit brighter --- Shared/Favicons/ColorHash.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Favicons/ColorHash.swift b/Shared/Favicons/ColorHash.swift index 650a70b76..b275d0c59 100644 --- a/Shared/Favicons/ColorHash.swift +++ b/Shared/Favicons/ColorHash.swift @@ -18,7 +18,7 @@ import AppKit public class ColorHash { - public static let defaultLS = [CGFloat(0.35), CGFloat(0.5), CGFloat(0.65)] + public static let defaultLS = [CGFloat(0.45), CGFloat(0.6), CGFloat(0.75)] let seed = CGFloat(131.0) let seed2 = CGFloat(137.0) let maxSafeInteger = 9007199254740991.0 / CGFloat(137.0) From 7316ceed9d781ade6f10c4daa36e08b1829aef87 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 08:07:12 -0500 Subject: [PATCH 026/189] Use newish keyboard modifier for username/email fields. Issue #1065 --- iOS/Settings/Account/SettingsFeedbinAccountView.swift | 4 +++- iOS/Settings/Account/SettingsReaderAPIAccountView.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/iOS/Settings/Account/SettingsFeedbinAccountView.swift b/iOS/Settings/Account/SettingsFeedbinAccountView.swift index 1dad3a572..511318a81 100644 --- a/iOS/Settings/Account/SettingsFeedbinAccountView.swift +++ b/iOS/Settings/Account/SettingsFeedbinAccountView.swift @@ -28,7 +28,9 @@ struct SettingsFeedbinAccountView : View { Spacer() } ) { - TextField("Email", text: $viewModel.email).textContentType(.emailAddress) + TextField("Email", text: $viewModel.email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) SecureField("Password", text: $viewModel.password) } Section(footer: diff --git a/iOS/Settings/Account/SettingsReaderAPIAccountView.swift b/iOS/Settings/Account/SettingsReaderAPIAccountView.swift index c091fbdc8..9e4218e9a 100644 --- a/iOS/Settings/Account/SettingsReaderAPIAccountView.swift +++ b/iOS/Settings/Account/SettingsReaderAPIAccountView.swift @@ -29,7 +29,9 @@ struct SettingsReaderAPIAccountView : View { Spacer() } ) { - TextField("Email", text: $viewModel.email).textContentType(.username) + TextField("Email", text: $viewModel.email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) SecureField("Password", text: $viewModel.password) TextField("API URL:", text: $viewModel.apiURL).textContentType(.URL) } From d59b5016debf7c12b940b70486682b76a6ed5497 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 11:42:11 -0500 Subject: [PATCH 027/189] Fix feed inspector name editing --- iOS/Inspector/FeedInspectorView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift index 79b22175a..95bc6b080 100644 --- a/iOS/Inspector/FeedInspectorView.swift +++ b/iOS/Inspector/FeedInspectorView.swift @@ -96,11 +96,15 @@ struct FeedInspectorView : View { var name: String { get { - return feed.editedName ?? feed.name ?? "" + return feed.editedName ?? "" } set { objectWillChange.send() - feed.editedName = newValue + if newValue.isEmpty { + feed.editedName = nil + } else { + feed.editedName = newValue + } } } From a7bee3a6d51bdfe40896b62c8159574bd2454221 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 12:36:25 -0500 Subject: [PATCH 028/189] Animate navigation for First Unread. Issue #1071 --- iOS/SceneCoordinator.swift | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 1a54b158a..61da29f00 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -556,11 +556,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func selectArticle(_ article: Article?, automated: Bool = true) { guard article != currentArticle else { return } - articleExtractor?.cancel() - articleExtractor = nil - isShowingExtractedArticle = false - articleViewController?.articleExtractorButtonState = .off - + stopArticleExtractor() currentArticle = article activityManager.reading(currentArticle) @@ -576,10 +572,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return } + let currentArticleViewController: ArticleViewController if articleViewController == nil { - let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) - articleViewController.coordinator = self - installArticleController(articleViewController, automated: automated) + currentArticleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) + currentArticleViewController.coordinator = self + installArticleController(currentArticleViewController, automated: automated) + } else { + currentArticleViewController = articleViewController! } if automated { @@ -588,9 +587,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { if article!.feed?.isArticleExtractorAlwaysOn ?? false { startArticleExtractorForCurrentLink() - articleViewController?.state = .loading + currentArticleViewController.state = .loading } else { - articleViewController?.state = .article(article!) + currentArticleViewController.state = .article(article!) } markArticles(Set([article!]), statusKey: .read, flag: true) @@ -828,10 +827,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } guard articleExtractor?.state != .processing else { - articleExtractor?.cancel() - articleExtractor = nil - isShowingExtractedArticle = false - articleViewController?.articleExtractorButtonState = .off + stopArticleExtractor() articleViewController?.state = .article(article) return } @@ -933,7 +929,9 @@ extension SceneCoordinator: UINavigationControllerDelegate { // If we are using a phone and navigate away from the detail, clear up the article resources (including activity) if viewController === masterTimelineViewController && !isThreePanelMode && rootSplitViewController.isCollapsed { - selectArticle(nil) + stopArticleExtractor() + currentArticle = nil + activityManager.invalidateReading() } } @@ -1180,7 +1178,7 @@ private extension SceneCoordinator { for i in startingRow..(), animate: true) From 2c3f665b582f204dab83c92bb5fd7b8d979fcfa0 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 13:32:54 -0500 Subject: [PATCH 029/189] Add accessibility labels to custom disclosure button --- iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift index 25d481c2d..5f684ce8f 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift @@ -117,8 +117,10 @@ class MasterFeedTableViewCell : NNWTableViewCell { UIView.animate(withDuration: duration) { if self.isDisclosureExpanded { + self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder") self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796) } else { + self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder") self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0) } } From ed2257a4f4ed04f49db2c08fc551aad1232e28d3 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Sep 2019 14:04:18 -0500 Subject: [PATCH 030/189] Reenable background iOS screenshooting --- iOS/RootSplitViewController.swift | 4 +--- iOS/SceneCoordinator.swift | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index 63a1965ea..89fb623f3 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -15,9 +15,7 @@ class RootSplitViewController: UISplitViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { coordinator.animate(alongsideTransition: { [weak self] context in - if UIApplication.shared.applicationState != .background { - self?.coordinator.configureThreePanelMode(for: size) - } + self?.coordinator.configureThreePanelMode(for: size) }) } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 61da29f00..3111419d1 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -910,7 +910,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { extension SceneCoordinator: UISplitViewControllerDelegate { func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool { - return masterTimelineViewController == nil + return currentArticle == nil } } From 1613a374d2d011a72c92ebe14ff256e96aba9ac0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 30 Sep 2019 13:33:52 -0700 Subject: [PATCH 031/189] Update version number. --- xcconfig/NetNewsWire_target.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcconfig/NetNewsWire_target.xcconfig b/xcconfig/NetNewsWire_target.xcconfig index 13b3e20ce..467010cfa 100644 --- a/xcconfig/NetNewsWire_target.xcconfig +++ b/xcconfig/NetNewsWire_target.xcconfig @@ -29,8 +29,8 @@ PROVISIONING_PROFILE_SPECIFIER = #include? "../../SharedXcodeSettings/DeveloperSettings.xcconfig" // High Level Settings common to both the Mac application and any extensions we bundle with it -MARKETING_VERSION = 5.0.2 -CURRENT_PROJECT_VERSION = 2615 +MARKETING_VERSION = 5.0.3b1 +CURRENT_PROJECT_VERSION = 2616 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES COMBINE_HIDPI_IMAGES = YES From 6ad1a21a74e693c0c665bf546ea6e09a155217a9 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 30 Sep 2019 14:32:18 -0700 Subject: [PATCH 032/189] Update appcast. --- Appcasts/netnewswire-beta.xml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Appcasts/netnewswire-beta.xml b/Appcasts/netnewswire-beta.xml index 105aefd2a..e270c3370 100755 --- a/Appcasts/netnewswire-beta.xml +++ b/Appcasts/netnewswire-beta.xml @@ -6,6 +6,32 @@ Most recent NetNewsWire changes with links to updates. en + + NetNewsWire 5.0.3b1 + Performance enhancement: fetching articles from the database is faster, and sometimes much faster.

+ +

Performance enhancement: syncing could block the main thread more than it should. We moved JSON decoding to a background thread, which fixes this. This is particularly noticeable during an initial sync.

+ +

Keyboard shortcuts: the 's' key toggles starred status. The 'r' and 'u' keys now both toggle read status (instead of setting read and unread status, respectively).

+ +

Articles view: articles where the feed icon is quite large would be slow to render — now they render as fast as other articles.

+ +

Articles view: a bug where keyboard shortcuts wouldn’t work after giving the articles view focus has been fixed.

+ +

Articles view: YouTube videos could end up small. Fixed.

+ +

Articles view: fixed a bug scaling images to fit in the view.

+ +

Feedbin syncing: fixed a bug where renaming a tag on the Feedbin site would result in feeds in NNW ending up at the top level.

+ +

Help menu: fixed the expired Slack link.

+ ]]>
+ Mon, 30 Sep 2019 13:56:00 -0700 + + 10.14.4 +
+ NetNewsWire 5.0.2 Date: Mon, 30 Sep 2019 20:01:02 -0500 Subject: [PATCH 033/189] Add unread count to the timeline --- NetNewsWire.xcodeproj/project.pbxproj | 4 +++ .../Cell/MasterFeedUnreadCountView.swift | 23 +++++++----- .../MasterTimelineTitleView.swift | 1 + .../MasterTimelineTitleView.xib | 13 +++++-- .../MasterTimelineUnreadCountView.swift | 35 +++++++++++++++++++ .../MasterTimelineViewController.swift | 10 +++++- 6 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 iOS/MasterTimeline/MasterTimelineUnreadCountView.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index f3789f111..9b35dae91 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -217,6 +217,7 @@ 51FA73AB2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; }; 51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */; }; 51FD40C72341555A00880194 /* UIImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD40BD2341555600880194 /* UIImage-Extensions.swift */; }; + 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; }; @@ -897,6 +898,7 @@ 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorConfig.swift; sourceTree = ""; }; 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; 51FD40BD2341555600880194 /* UIImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage-Extensions.swift"; sourceTree = ""; }; + 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineUnreadCountView.swift; sourceTree = ""; }; 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = ""; }; 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; @@ -1369,6 +1371,7 @@ 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */, 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */, 5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */, + 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */, 51C4526F2265091600C03939 /* Cell */, ); path = MasterTimeline; @@ -2791,6 +2794,7 @@ 517630232336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift in Sources */, 51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */, 51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */, + 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */, 5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */, 51AF45E123246731001742EF /* SettingsAccountLabelView.swift in Sources */, 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */, diff --git a/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift b/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift index fdddb629a..09b1d5d69 100644 --- a/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift +++ b/iOS/MasterFeed/Cell/MasterFeedUnreadCountView.swift @@ -10,19 +10,26 @@ import UIKit class MasterFeedUnreadCountView : UIView { - private let padding = UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0) - private let cornerRadius = 8.0 - private let bgColor = UIColor.darkGray - private let textColor = UIColor.white - private var textAttributes: [NSAttributedString.Key: AnyObject] { + var padding: UIEdgeInsets { + return UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0) + } + + let cornerRadius = 8.0 + let bgColor = UIColor.darkGray + var textColor: UIColor { + return UIColor.white + } + + var textAttributes: [NSAttributedString.Key: AnyObject] { let textFont = UIFont.preferredFont(forTextStyle: .caption1) return [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()] } - private var textSizeCache = [Int: CGSize]() + var textSizeCache = [Int: CGSize]() var unreadCount = 0 { didSet { contentSizeIsValid = false + invalidateIntrinsicContentSize() setNeedsDisplay() } } @@ -69,7 +76,7 @@ class MasterFeedUnreadCountView : UIView { return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) } - private func textSize() -> CGSize { + func textSize() -> CGSize { if unreadCount < 1 { return CGSize.zero @@ -88,7 +95,7 @@ class MasterFeedUnreadCountView : UIView { } - private func textRect() -> CGRect { + func textRect() -> CGRect { let size = textSize() var r = CGRect.zero diff --git a/iOS/MasterTimeline/MasterTimelineTitleView.swift b/iOS/MasterTimeline/MasterTimelineTitleView.swift index 2861ce634..03e68a1af 100644 --- a/iOS/MasterTimeline/MasterTimelineTitleView.swift +++ b/iOS/MasterTimeline/MasterTimelineTitleView.swift @@ -20,5 +20,6 @@ class MasterTimelineTitleView: UIView { } @IBOutlet weak var label: UILabel! + @IBOutlet weak var unreadCountView: MasterTimelineUnreadCountView! } diff --git a/iOS/MasterTimeline/MasterTimelineTitleView.xib b/iOS/MasterTimeline/MasterTimelineTitleView.xib index 5abe26c5a..ad66ffdef 100644 --- a/iOS/MasterTimeline/MasterTimelineTitleView.xib +++ b/iOS/MasterTimeline/MasterTimelineTitleView.xib @@ -20,20 +20,26 @@ -