Merge branch 'ios-candidate'

This commit is contained in:
Maurice Parker 2021-05-25 20:12:50 -05:00
commit f9af3c786b
43 changed files with 398 additions and 286 deletions

View File

@ -13,7 +13,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(url: "../Articles", .upToNextMajor(from: "1.0.0")), .package(url: "../Articles", .upToNextMajor(from: "1.0.0")),
.package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")), .package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")),

View File

@ -180,10 +180,14 @@ final class CloudKitAccountZone: CloudKitZone {
} }
case .failure(let error): case .failure(let error):
if let ckError = ((error as? CloudKitError)?.error as? CKError), ckError.code == .unknownItem {
completion(.success(true))
} else {
completion(.failure(error)) completion(.failure(error))
} }
} }
} }
}
func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else { guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {

View File

@ -16,7 +16,8 @@ import Articles
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String) private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String)
private var unclaimedWebFeeds = [String: [UnclaimedWebFeed]]() private var newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]()
private var existingUnclaimedWebFeeds = [String: [WebFeed]]()
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
@ -75,7 +76,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
if let container = account.existingContainer(withExternalID: containerExternalID) { if let container = account.existingContainer(withExternalID: containerExternalID) {
createWebFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, container: container) createWebFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, container: container)
} else { } else {
addUnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, containerExternalID: containerExternalID) addNewUnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, containerExternalID: containerExternalID)
} }
} }
} }
@ -106,19 +107,27 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
folder?.externalID = record.externalID folder?.externalID = record.externalID
} }
if let folder = folder, let containerExternalID = folder.externalID, let unclaimedWebFeeds = unclaimedWebFeeds[containerExternalID] { guard let container = folder, let containerExternalID = container.externalID else { return }
for unclaimedWebFeed in unclaimedWebFeeds {
createWebFeedIfNecessary(url: unclaimedWebFeed.url, if let newUnclaimedWebFeeds = newUnclaimedWebFeeds[containerExternalID] {
name: unclaimedWebFeed.name, for newUnclaimedWebFeed in newUnclaimedWebFeeds {
editedName: unclaimedWebFeed.editedName, createWebFeedIfNecessary(url: newUnclaimedWebFeed.url,
homePageURL: unclaimedWebFeed.homePageURL, name: newUnclaimedWebFeed.name,
webFeedExternalID: unclaimedWebFeed.webFeedExternalID, editedName: newUnclaimedWebFeed.editedName,
container: folder) homePageURL: newUnclaimedWebFeed.homePageURL,
webFeedExternalID: newUnclaimedWebFeed.webFeedExternalID,
container: container)
} }
self.unclaimedWebFeeds.removeValue(forKey: containerExternalID) self.newUnclaimedWebFeeds.removeValue(forKey: containerExternalID)
} }
if let existingUnclaimedWebFeeds = existingUnclaimedWebFeeds[containerExternalID] {
for existingUnclaimedWebFeed in existingUnclaimedWebFeeds {
container.addWebFeed(existingUnclaimedWebFeed)
}
self.existingUnclaimedWebFeeds.removeValue(forKey: containerExternalID)
}
} }
func removeContainer(_ externalID: String) { func removeContainer(_ externalID: String) {
@ -152,6 +161,8 @@ private extension CloudKitAcountZoneDelegate {
case .insert(_, let externalID, _): case .insert(_, let externalID, _):
if let container = account.existingContainer(withExternalID: externalID) { if let container = account.existingContainer(withExternalID: externalID) {
container.addWebFeed(webFeed) container.addWebFeed(webFeed)
} else {
addExistingUnclaimedWebFeed(webFeed, containerExternalID: externalID)
} }
} }
} }
@ -170,14 +181,25 @@ private extension CloudKitAcountZoneDelegate {
container.addWebFeed(webFeed) container.addWebFeed(webFeed)
} }
func addUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) { func addNewUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) {
if var unclaimedWebFeeds = self.unclaimedWebFeeds[containerExternalID] { if var unclaimedWebFeeds = self.newUnclaimedWebFeeds[containerExternalID] {
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID)) unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
self.unclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
} else { } else {
var unclaimedWebFeeds = [UnclaimedWebFeed]() var unclaimedWebFeeds = [UnclaimedWebFeed]()
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID)) unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
self.unclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
}
}
func addExistingUnclaimedWebFeed(_ webFeed: WebFeed, containerExternalID: String) {
if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] {
unclaimedWebFeeds.append(webFeed)
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
} else {
var unclaimedWebFeeds = [WebFeed]()
unclaimedWebFeeds.append(webFeed)
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
} }
} }

View File

@ -78,21 +78,21 @@ private extension TwitterStatus {
var html = String() var html = String()
var prevIndex = displayStartIndex var prevIndex = displayStartIndex
var emojiOffset = 0 var unicodeScalarOffset = 0
for entity in entities { for entity in entities {
// The twitter indices are messed up by emoji with more than one scalar, we are going to adjust for that here. // The twitter indices are messed up by characters with more than one scalar, we are going to adjust for that here.
let emojiEndIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex let endIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex
if prevIndex < emojiEndIndex { if prevIndex < endIndex {
let emojis = String(text[prevIndex..<emojiEndIndex]).emojis let characters = String(text[prevIndex..<endIndex])
for emoji in emojis { for character in characters {
emojiOffset += emoji.unicodeScalars.count - 1 unicodeScalarOffset += character.unicodeScalars.count - 1
} }
} }
let offsetStartIndex = entity.startIndex - emojiOffset let offsetStartIndex = entity.startIndex - unicodeScalarOffset
let offsetEndIndex = entity.endIndex - emojiOffset let offsetEndIndex = entity.endIndex - unicodeScalarOffset
let entityStartIndex = text.index(text.startIndex, offsetBy: offsetStartIndex, limitedBy: text.endIndex) ?? text.startIndex let entityStartIndex = text.index(text.startIndex, offsetBy: offsetStartIndex, limitedBy: text.endIndex) ?? text.startIndex
let entityEndIndex = text.index(text.startIndex, offsetBy: offsetEndIndex, limitedBy: text.endIndex) ?? text.endIndex let entityEndIndex = text.index(text.startIndex, offsetBy: offsetEndIndex, limitedBy: text.endIndex) ?? text.endIndex

View File

@ -10,7 +10,7 @@ import Foundation
import RSParser import RSParser
import RSCore import RSCore
final class FeedbinEntry: Codable { final class FeedbinEntry: Decodable {
let articleID: Int let articleID: Int
let feedID: Int let feedID: Int
@ -50,14 +50,25 @@ final class FeedbinEntry: Codable {
} }
} }
struct FeedbinEntryJSONFeed: Codable { struct FeedbinEntryJSONFeed: Decodable {
let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor? let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case jsonFeedAuthor = "author" case jsonFeedAuthor = "author"
} }
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
jsonFeedAuthor = try container.decode(FeedbinEntryJSONFeedAuthor.self, forKey: .jsonFeedAuthor)
} catch {
jsonFeedAuthor = nil
}
} }
struct FeedbinEntryJSONFeedAuthor: Codable { }
struct FeedbinEntryJSONFeedAuthor: Decodable {
let url: String? let url: String?
let avatarURL: String? let avatarURL: String?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {

View File

@ -310,7 +310,7 @@ final class FeedlyAPICaller {
request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in send(request: request, resultType: Optional<FeedlyCollection>.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
switch result { switch result {
case .success(let (httpResponse, _)): case .success(let (httpResponse, _)):
if httpResponse.statusCode == 200 { if httpResponse.statusCode == 200 {

View File

@ -133,7 +133,7 @@ final class NewsBlurAPICaller: NSObject {
URLQueryItem(name: "page", value: String(page)), URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "order", value: "newest"), URLQueryItem(name: "order", value: "newest"),
URLQueryItem(name: "read_filter", value: "all"), URLQueryItem(name: "read_filter", value: "all"),
URLQueryItem(name: "include_hidden", value: "true"), URLQueryItem(name: "include_hidden", value: "false"),
URLQueryItem(name: "include_story_content", value: "true"), URLQueryItem(name: "include_story_content", value: "true"),
]) ])
@ -150,7 +150,7 @@ final class NewsBlurAPICaller: NSObject {
func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) { func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
let url = baseURL let url = baseURL
.appendingPathComponent("reader/river_stories") .appendingPathComponent("reader/river_stories")
.appendingQueryItem(.init(name: "include_hidden", value: "true"))? .appendingQueryItem(.init(name: "include_hidden", value: "false"))?
.appendingQueryItems(hashes.map { .appendingQueryItems(hashes.map {
URLQueryItem(name: "h", value: $0.hash) URLQueryItem(name: "h", value: $0.hash)
}) })

View File

@ -14,10 +14,24 @@ import SyncDatabase
import os.log import os.log
import Secrets import Secrets
public enum ReaderAPIAccountDelegateError: String, Error { public enum ReaderAPIAccountDelegateError: LocalizedError {
case unknown = "An unknown error occurred." case unknown
case invalidParameter = "There was an invalid parameter passed." case invalidParameter
case invalidResponse = "There was an invalid response from the server." case invalidResponse
case urlNotFound
public var errorDescription: String? {
switch self {
case .unknown:
return NSLocalizedString("An unexpected error occurred.", comment: "An unexpected error occurred.")
case .invalidParameter:
return NSLocalizedString("An invalid parameter was passed.", comment: "An invalid parameter was passed.")
case .invalidResponse:
return NSLocalizedString("There was an invalid response from the server.", comment: "There was an invalid response from the server.")
case .urlNotFound:
return NSLocalizedString("The API URL wasn't found.", comment: "The API URL wasn't found.")
}
}
} }
final class ReaderAPIAccountDelegate: AccountDelegate { final class ReaderAPIAccountDelegate: AccountDelegate {

View File

@ -132,9 +132,13 @@ final class ReaderAPICaller: NSObject {
completion(.success(self.credentials)) completion(.success(self.credentials))
case .failure(let error): case .failure(let error):
if let transportError = error as? TransportError, case .httpError(let code) = transportError, code == 404 {
completion(.failure(ReaderAPIAccountDelegateError.urlNotFound))
} else {
completion(.failure(error)) completion(.failure(error))
} }
} }
}
} }

View File

@ -15,7 +15,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
.package(url: "../Articles", .upToNextMajor(from: "1.0.0")), .package(url: "../Articles", .upToNextMajor(from: "1.0.0")),
], ],
targets: [ targets: [

View File

@ -149,18 +149,7 @@ private extension WebFeedInspectorViewController {
guard let feed = feed, let iconView = iconView else { guard let feed = feed, let iconView = iconView else {
return return
} }
iconView.iconImage = IconImageCache.shared.imageForFeed(feed)
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) {
iconView.iconImage = feedIcon
return
}
if let favicon = appDelegate.faviconDownloader.favicon(for: feed) {
iconView.iconImage = favicon
return
}
iconView.iconImage = feed.smallIcon
} }
func updateName() { func updateName() {

View File

@ -768,7 +768,7 @@ private extension SidebarViewController {
} }
func imageFor(_ node: Node) -> IconImage? { func imageFor(_ node: Node) -> IconImage? {
if let feed = node.representedObject as? WebFeed, let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) { if let feed = node.representedObject as? WebFeed, let feedIcon = IconImageCache.shared.imageForFeed(feed) {
return feedIcon return feedIcon
} }
if let smallIconProvider = node.representedObject as? SmallIconProvider { if let smallIconProvider = node.representedObject as? SmallIconProvider {

View File

@ -38,7 +38,7 @@ extension Article: PasteboardWriterOwner {
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] { func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
var types = [ArticlePasteboardWriter.articleUTIType] var types = [ArticlePasteboardWriter.articleUTIType]
if let link = article.preferredLink, let _ = URL(string: link) { if let _ = article.preferredURL {
types += [.URL] types += [.URL]
} }
types += [.string, .html, ArticlePasteboardWriter.articleUTIInternalType] types += [.string, .html, ArticlePasteboardWriter.articleUTIInternalType]

View File

@ -886,28 +886,7 @@ extension TimelineViewController: NSTableViewDelegate {
if !showIcons { if !showIcons {
return nil return nil
} }
return IconImageCache.shared.imageForArticle(article)
if let authors = article.authors {
for author in authors {
if let image = avatarForAuthor(author) {
return image
}
}
}
guard let feed = article.webFeed else {
return nil
}
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) {
return feedIcon
}
if let favicon = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
return favicon
}
return FaviconGenerator.favicon(feed)
} }
private func avatarForAuthor(_ author: Author) -> IconImage? { private func avatarForAuthor(_ author: Author) -> IconImage? {

View File

@ -107,7 +107,7 @@ struct ArticleToolbarModifier: ViewModifier {
.disabled(sceneModel.shareButtonState == nil) .disabled(sceneModel.shareButtonState == nil)
.help("Share") .help("Share")
.sheet(isPresented: $showActivityView) { .sheet(isPresented: $showActivityView) {
if let article = sceneModel.selectedArticles.first, let link = article.preferredLink, let url = URL(string: link) { if let article = sceneModel.selectedArticles.first, let url = article.preferredURL {
ActivityViewController(title: article.title, url: url) ActivityViewController(title: article.title, url: url)
} }
} }

View File

@ -41,20 +41,6 @@ final class FeedIconImageLoader: ObservableObject {
private extension FeedIconImageLoader { private extension FeedIconImageLoader {
func fetchImage() { func fetchImage() {
if let webFeed = feed as? WebFeed { image = IconImageCache.shared.imageForFeed(feed)
if let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) {
image = feedIconImage
return
}
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
image = faviconImage
return
} }
} }
if let smallIconProvider = feed as? SmallIconProvider {
image = smallIconProvider.smallIcon
}
}
}

View File

@ -249,12 +249,11 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
} }
func openIndicatedArticleInBrowser(_ article: Article) { func openIndicatedArticleInBrowser(_ article: Article) {
guard let link = article.preferredLink else { return }
#if os(macOS) #if os(macOS)
guard let link = article.preferredLink else { return }
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false) Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
#else #else
guard let url = URL(string: link) else { return } guard let url = article.preferredURL else { return }
UIApplication.shared.open(url, options: [:]) UIApplication.shared.open(url, options: [:])
#endif #endif
} }

View File

@ -1,9 +1,9 @@
{\rtf1\ansi\ansicpg1252\cocoartf2511 {\rtf1\ansi\ansicpg1252\cocoartf2513
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
\margl1440\margr1440\vieww9000\viewh8400\viewkind0 \margl1440\margr1440\vieww9000\viewh8400\viewkind0
\deftab720 \deftab720
\pard\pardeftab720\li354\fi-355\sa60\partightenfactor0 \pard\pardeftab720\sa60\partightenfactor0
\f0\fs28 \cf2 NetNewsWire 5.0 is dedicated to all the people who showed up to help with code, design, HTML, documentation, icons, testing, and just to help talk things over and think things through. This app\'92s for you!} \f0\fs22 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.}

View File

@ -24,7 +24,7 @@ struct SettingsAboutView: View {
Section(header: Text("THANKS")) { Section(header: Text("THANKS")) {
AttributedStringView(string: self.viewModel.thanks, preferredMaxLayoutWidth: geometry.size.width - 20) AttributedStringView(string: self.viewModel.thanks, preferredMaxLayoutWidth: geometry.size.width - 20)
} }
Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2020 BrentSimmons").font(.footnote)) { Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2021 Brent Simmons").font(.footnote)) {
AttributedStringView(string: self.viewModel.dedication, preferredMaxLayoutWidth: geometry.size.width - 20) AttributedStringView(string: self.viewModel.dedication, preferredMaxLayoutWidth: geometry.size.width - 20)
} }
}.listStyle(InsetGroupedListStyle()) }.listStyle(InsetGroupedListStyle())

View File

@ -1015,6 +1015,9 @@
844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; }; 844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; };
845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; }; 845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; };
845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; }; 845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; };
8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; };
8454C3F8263F3AD400E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; };
8454C3FD263F3AD600E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; };
845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; 845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; };
845A29221FC9251E007B49E3 /* SidebarCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */; }; 845A29221FC9251E007B49E3 /* SidebarCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */; };
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */; }; 845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */; };
@ -1901,6 +1904,7 @@
844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = SidebarKeyboardShortcuts.plist; sourceTree = "<group>"; }; 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = SidebarKeyboardShortcuts.plist; sourceTree = "<group>"; };
845213221FCA5B10003B6E93 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; }; 845213221FCA5B10003B6E93 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; };
845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = TimelineKeyboardShortcuts.plist; sourceTree = "<group>"; }; 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = TimelineKeyboardShortcuts.plist; sourceTree = "<group>"; };
8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImageCache.swift; sourceTree = "<group>"; };
845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.swift; sourceTree = "<group>"; }; 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.swift; sourceTree = "<group>"; };
845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellLayout.swift; sourceTree = "<group>"; }; 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellLayout.swift; sourceTree = "<group>"; };
845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellAppearance.swift; sourceTree = "<group>"; }; 845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellAppearance.swift; sourceTree = "<group>"; };
@ -3436,6 +3440,7 @@
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */, 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */,
51C4CFEF24D37D1F00AF9874 /* Secrets.swift */, 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */,
511B9805237DCAC90028BCAA /* UserInfoKey.swift */, 511B9805237DCAC90028BCAA /* UserInfoKey.swift */,
8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */,
51C452AD2265102800C03939 /* Timeline */, 51C452AD2265102800C03939 /* Timeline */,
84702AB31FA27AE8006B8943 /* Commands */, 84702AB31FA27AE8006B8943 /* Commands */,
51934CCC231078DC006127BE /* Activity */, 51934CCC231078DC006127BE /* Activity */,
@ -5150,6 +5155,7 @@
65ED4007235DEF6C0081F399 /* AddFeedController.swift in Sources */, 65ED4007235DEF6C0081F399 /* AddFeedController.swift in Sources */,
65ED4008235DEF6C0081F399 /* AccountRefreshTimer.swift in Sources */, 65ED4008235DEF6C0081F399 /* AccountRefreshTimer.swift in Sources */,
65ED4009235DEF6C0081F399 /* SidebarStatusBarView.swift in Sources */, 65ED4009235DEF6C0081F399 /* SidebarStatusBarView.swift in Sources */,
8454C3FD263F3AD600E3F9C7 /* IconImageCache.swift in Sources */,
65ED400A235DEF6C0081F399 /* SearchTimelineFeedDelegate.swift in Sources */, 65ED400A235DEF6C0081F399 /* SearchTimelineFeedDelegate.swift in Sources */,
65ED400B235DEF6C0081F399 /* TodayFeedDelegate.swift in Sources */, 65ED400B235DEF6C0081F399 /* TodayFeedDelegate.swift in Sources */,
65ED400C235DEF6C0081F399 /* FolderInspectorViewController.swift in Sources */, 65ED400C235DEF6C0081F399 /* FolderInspectorViewController.swift in Sources */,
@ -5375,6 +5381,7 @@
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */,
D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */, D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */,
8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */,
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
@ -5545,6 +5552,7 @@
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */, 841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */,
D5E4CC54202C1361009B4FFC /* AppDelegate+Scriptability.swift in Sources */, D5E4CC54202C1361009B4FFC /* AppDelegate+Scriptability.swift in Sources */,
8454C3F8263F3AD400E3F9C7 /* IconImageCache.swift in Sources */,
518651B223555EB20078E021 /* NNW3Document.swift in Sources */, 518651B223555EB20078E021 /* NNW3Document.swift in Sources */,
D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */, D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */,
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */, D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */,

View File

@ -248,9 +248,8 @@ private extension ActivityManager {
attributeSet.title = feed.nameForDisplay attributeSet.title = feed.nameForDisplay
attributeSet.keywords = makeKeywords(feed.nameForDisplay) attributeSet.keywords = makeKeywords(feed.nameForDisplay)
attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: feed) attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: feed)
if let iconImage = appDelegate.webFeedIconDownloader.icon(for: feed) {
attributeSet.thumbnailData = iconImage.image.dataRepresentation() if let iconImage = IconImageCache.shared.imageForFeed(feed) {
} else if let iconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
attributeSet.thumbnailData = iconImage.image.dataRepresentation() attributeSet.thumbnailData = iconImage.image.dataRepresentation()
} }

View File

@ -157,7 +157,7 @@
document.addEventListener("click", (ev) => document.addEventListener("click", (ev) =>
{ {
if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return; if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return")) return; if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return, .footnotes a[href*='#fn'], .footnotes a[href^='#']")) return;
const id = idFromHash(ev.target); const id = idFromHash(ev.target);
if (!id) return; if (!id) return;
const fnref = document.getElementById(id); const fnref = document.getElementById(id);

View File

@ -382,7 +382,8 @@ img[src*="share-buttons"] {
.newsfoot-footnote-popover .reversefootnote, .newsfoot-footnote-popover .reversefootnote,
.newsfoot-footnote-popover .footnoteBackLink, .newsfoot-footnote-popover .footnoteBackLink,
.newsfoot-footnote-popover .footnote-return { .newsfoot-footnote-popover .footnote-return,
.newsfoot-footnote-popover a[href*='#fn'] {
display: none; display: none;
} }

View File

@ -27,6 +27,7 @@ final class MarkStatusCommand: UndoableCommand {
// Filter out articles that already have the desired status or can't be marked. // Filter out articles that already have the desired status or can't be marked.
let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag) let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag)
if articlesToMark.isEmpty { if articlesToMark.isEmpty {
completion?()
return nil return nil
} }
self.articles = Set(articlesToMark) self.articles = Set(articlesToMark)

View File

@ -56,6 +56,18 @@ extension Article {
return nil return nil
} }
var preferredURL: URL? {
guard let link = preferredLink else { return nil }
// If required, we replace any space characters to handle malformed links that are otherwise percent
// encoded but contain spaces. For performance reasons, only try this if initial URL init fails.
if let url = URL(string: link) {
return url
} else if let url = URL(string: link.replacingOccurrences(of: " ", with: "%20")) {
return url
}
return nil
}
var body: String? { var body: String? {
return contentHTML ?? contentText ?? summary return contentHTML ?? contentText ?? summary
} }
@ -84,32 +96,7 @@ extension Article {
} }
func iconImage() -> IconImage? { func iconImage() -> IconImage? {
if let authors = authors, authors.count == 1, let author = authors.first { return IconImageCache.shared.imageForArticle(self)
if let image = appDelegate.authorAvatarDownloader.image(for: author) {
return image
}
}
if let authors = webFeed?.authors, authors.count == 1, let author = authors.first {
if let image = appDelegate.authorAvatarDownloader.image(for: author) {
return image
}
}
guard let webFeed = webFeed else {
return nil
}
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed)
if feedIconImage != nil {
return feedIconImage
}
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
return faviconImage
}
return FaviconGenerator.favicon(webFeed)
} }
func iconImageUrl(webFeed: WebFeed) -> URL? { func iconImageUrl(webFeed: WebFeed) -> URL? {

129
Shared/IconImageCache.swift Normal file
View File

@ -0,0 +1,129 @@
//
// IconImageCache.swift
// NetNewsWire-iOS
//
// Created by Brent Simmons on 5/2/21.
// Copyright © 2021 Ranchero Software. All rights reserved.
//
import Foundation
import Account
import Articles
class IconImageCache {
static var shared = IconImageCache()
private var smartFeedIconImageCache = [FeedIdentifier: IconImage]()
private var webFeedIconImageCache = [FeedIdentifier: IconImage]()
private var faviconImageCache = [FeedIdentifier: IconImage]()
private var smallIconImageCache = [FeedIdentifier: IconImage]()
private var authorIconImageCache = [Author: IconImage]()
func imageFor(_ feedID: FeedIdentifier) -> IconImage? {
if let smartFeed = SmartFeedsController.shared.find(by: feedID) {
return imageForFeed(smartFeed)
}
if let feed = AccountManager.shared.existingFeed(with: feedID) {
return imageForFeed(feed)
}
return nil
}
func imageForFeed(_ feed: Feed) -> IconImage? {
guard let feedID = feed.feedID else {
return nil
}
if let smartFeed = feed as? PseudoFeed {
return imageForSmartFeed(smartFeed, feedID)
}
if let webFeed = feed as? WebFeed, let iconImage = imageForWebFeed(webFeed, feedID) {
return iconImage
}
if let smallIconProvider = feed as? SmallIconProvider {
return imageForSmallIconProvider(smallIconProvider, feedID)
}
return nil
}
func imageForArticle(_ article: Article) -> IconImage? {
if let iconImage = imageForAuthors(article.authors) {
return iconImage
}
guard let feed = article.webFeed else {
return nil
}
return imageForFeed(feed)
}
func emptyCache() {
smartFeedIconImageCache = [FeedIdentifier: IconImage]()
webFeedIconImageCache = [FeedIdentifier: IconImage]()
faviconImageCache = [FeedIdentifier: IconImage]()
smallIconImageCache = [FeedIdentifier: IconImage]()
authorIconImageCache = [Author: IconImage]()
}
}
private extension IconImageCache {
func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: FeedIdentifier) -> IconImage? {
if let iconImage = smartFeedIconImageCache[feedID] {
return iconImage
}
if let iconImage = smartFeed.smallIcon {
smartFeedIconImageCache[feedID] = iconImage
return iconImage
}
return nil
}
func imageForWebFeed(_ webFeed: WebFeed, _ feedID: FeedIdentifier) -> IconImage? {
if let iconImage = webFeedIconImageCache[feedID] {
return iconImage
}
if let iconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) {
webFeedIconImageCache[feedID] = iconImage
return iconImage
}
if let faviconImage = faviconImageCache[feedID] {
return faviconImage
}
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
faviconImageCache[feedID] = faviconImage
return faviconImage
}
return nil
}
func imageForSmallIconProvider(_ provider: SmallIconProvider, _ feedID: FeedIdentifier) -> IconImage? {
if let iconImage = smallIconImageCache[feedID] {
return iconImage
}
if let iconImage = provider.smallIcon {
smallIconImageCache[feedID] = iconImage
return iconImage
}
return nil
}
func imageForAuthors(_ authors: Set<Author>?) -> IconImage? {
guard let authors = authors, authors.count == 1, let author = authors.first else {
return nil
}
return imageForAuthor(author)
}
func imageForAuthor(_ author: Author) -> IconImage? {
if let iconImage = authorIconImageCache[author] {
return iconImage
}
if let iconImage = appDelegate.authorAvatarDownloader.image(for: author) {
authorIconImageCache[author] = iconImage
return iconImage
}
return nil
}
}

View File

@ -56,9 +56,7 @@ private extension WebFeedTreeControllerDelegate {
func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] { func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] {
return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in
if let feedID = feed.feedID, !filterExceptions.contains(feedID) && isReadFiltered && feed.unreadCount == 0 { // All Smart Feeds should remain visible despite the Hide Read Feeds setting
return nil
}
return parentNode.existingOrNewChildNode(with: feed as AnyObject) return parentNode.existingOrNewChildNode(with: feed as AnyObject)
} }
} }

View File

@ -0,0 +1,25 @@
# iOS Release Notes
### 6.0 TestFlight build 603 - 16 May 2021
Feedly: handle Feedly API change with return value on deleting a folder
NewsBlur: sync no longer includes items marked as hidden on NewsBlur
FreshRSS: form for adding account now suggests endpoing URL
FreshRSS: improved the error message for when the API URL cant be found
iCloud: retain existing feeds moved to a folder that doesnt exist yet (sync ordering issue)
Renamed a Delete Account button to Remove Account
iCloud: skip displaying an error message on deleting a feed that doesnt exist in iCloud
Preferences: Tweaked text explaining Feed Providers
Feeds list: context menu for smart feeds is back (regression fix)
Feeds list: all smart feeds remain visible despite Hide Read Feeds setting
Article view: fixed zoom issue on iPad on rotation
Article view: fixed bug where mark-read button on toolbar would flash on navigating to an unread article
Article view: made footnote detection more robust
Fixed regression on iPad where timeline and article wouldnt update after the selected feed was deleted
Sharing: handle feeds where the URL has unencoded space characters (why a feed would do that is beyond our ken)
### 6.0 TestFlight build 602 - 21 April 2021
Inoreader: dont call it so often, so we dont go over the API limits
Feedly: handle a specific case where Feedly started not returning a value we expected but didnt actually need (we were reporting it as an error to the user, but it wasnt)

View File

@ -50,6 +50,7 @@ class ReaderAPIAccountViewController: UITableViewController {
switch unwrappedAccountType { switch unwrappedAccountType {
case .freshRSS: case .freshRSS:
title = NSLocalizedString("FreshRSS", comment: "FreshRSS") title = NSLocalizedString("FreshRSS", comment: "FreshRSS")
apiURLTextField.placeholder = NSLocalizedString("API URL: fresh.rss.net/api/greader.php", comment: "FreshRSS API Helper")
case .inoreader: case .inoreader:
title = NSLocalizedString("InoReader", comment: "InoReader") title = NSLocalizedString("InoReader", comment: "InoReader")
case .bazQux: case .bazQux:

View File

@ -133,6 +133,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
shuttingDown = true shuttingDown = true
} }
func applicationDidEnterBackground(_ application: UIApplication) {
IconImageCache.shared.emptyCache()
}
// MARK: Notifications // MARK: Notifications
@objc func unreadCountDidChange(_ note: Notification) { @objc func unreadCountDidChange(_ note: Notification) {

View File

@ -79,6 +79,13 @@ class WebViewController: UIViewController {
} }
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// We need to reload the webview on the iPhone when rotation happens to clear out any old bad viewport sizes
if traitCollection.userInterfaceIdiom == .phone {
loadWebView()
}
}
// MARK: Notifications // MARK: Notifications
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) { @objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
@ -235,20 +242,14 @@ class WebViewController: UIViewController {
} }
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) { func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { guard let url = article?.preferredURL else { return }
return
}
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()]) let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()])
activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem
present(activityViewController, animated: true) present(activityViewController, animated: true)
} }
func openInAppBrowser() { func openInAppBrowser() {
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { guard let url = article?.preferredURL else { return }
return
}
let vc = SFSafariViewController(url: url) let vc = SFSafariViewController(url: url)
present(vc, animated: true) present(vc, animated: true)
} }

View File

@ -12,28 +12,19 @@ final class IconView: UIView {
var iconImage: IconImage? = nil { var iconImage: IconImage? = nil {
didSet { didSet {
if iconImage !== oldValue { guard iconImage !== oldValue else {
return
}
imageView.image = iconImage?.image imageView.image = iconImage?.image
if traitCollection.userInterfaceStyle == .dark {
if self.traitCollection.userInterfaceStyle == .dark { let isDark = iconImage?.isDark ?? false
if self.iconImage?.isDark ?? false { isDiscernable = !isDark
self.isDiscernable = false
self.setNeedsLayout()
} else {
self.isDiscernable = true
self.setNeedsLayout()
} }
} else { else {
if self.iconImage?.isBright ?? false { let isBright = iconImage?.isBright ?? false
self.isDiscernable = false isDiscernable = !isBright
self.setNeedsLayout()
} else {
self.isDiscernable = true
self.setNeedsLayout()
}
}
self.setNeedsLayout()
} }
setNeedsLayout()
} }
} }
@ -79,15 +70,10 @@ final class IconView: UIView {
override func layoutSubviews() { override func layoutSubviews() {
imageView.setFrameIfNotEqual(rectForImageView()) imageView.setFrameIfNotEqual(rectForImageView())
if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) { updateBackgroundColor()
backgroundColor = AppAssets.iconBackgroundColor
} else {
backgroundColor = nil
} }
} }
}
private extension IconView { private extension IconView {
func commonInit() { func commonInit() {
@ -125,4 +111,11 @@ private extension IconView {
return CGRect(x: 0.0, y: originY, width: viewSize.width, height: height) return CGRect(x: 0.0, y: originY, width: viewSize.width, height: height)
} }
private func updateBackgroundColor() {
if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) {
backgroundColor = AppAssets.iconBackgroundColor
} else {
backgroundColor = nil
}
}
} }

View File

@ -92,13 +92,13 @@ class AccountInspectorViewController: UITableViewController {
return return
} }
let title = NSLocalizedString("Delete Account", comment: "Delete Account") let title = NSLocalizedString("Remove Account", comment: "Remove Account")
let message: String = { let message: String = {
switch account.type { switch account.type {
case .feedly: case .feedly:
return NSLocalizedString("Are you sure you want to delete this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.", comment: "Log Out and Delete Account") return NSLocalizedString("Are you sure you want to remove this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.", comment: "Log Out and Remove Account")
default: default:
return NSLocalizedString("Are you sure you want to delete this account? This cannot be undone.", comment: "Delete Account") return NSLocalizedString("Are you sure you want to remove this account? This cannot be undone.", comment: "Remove Account")
} }
}() }()
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
@ -106,7 +106,7 @@ class AccountInspectorViewController: UITableViewController {
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
alertController.addAction(cancelAction) alertController.addAction(cancelAction)
let markTitle = NSLocalizedString("Delete", comment: "Delete") let markTitle = NSLocalizedString("Remove", comment: "Remove")
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
guard let self = self, let account = self.account else { return } guard let self = self, let account = self.account else { return }
AccountManager.shared.deleteAccount(account) AccountManager.shared.deleteAccount(account)

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -27,10 +27,10 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="19" translatesAutoresizingMaskIntoConstraints="NO" id="mQa-0W-eVS"> <stackView opaque="NO" contentMode="scaleToFill" spacing="19" translatesAutoresizingMaskIntoConstraints="NO" id="mQa-0W-eVS">
<rect key="frame" x="20" y="11" width="334" height="22"/> <rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name (Optional)" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="LUW-uv-piz"> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name (Optional)" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="LUW-uv-piz">
<rect key="frame" x="0.0" y="0.0" width="334" height="22"/> <rect key="frame" x="0.0" y="0.0" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words"/> <textInputTraits key="textInputTraits" autocapitalizationType="words"/>
</textField> </textField>
@ -52,7 +52,7 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-Gm-p4F"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-Gm-p4F">
<rect key="frame" x="20" y="11.5" width="48" height="21"/> <rect key="frame" x="20" y="13.5" width="40" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -119,7 +119,7 @@
<constraint firstAttribute="height" constant="44" id="WtN-fp-Ldt"/> <constraint firstAttribute="height" constant="44" id="WtN-fp-Ldt"/>
</constraints> </constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Delete Account"> <state key="normal" title="Remove Account">
<color key="titleColor" systemColor="systemRedColor"/> <color key="titleColor" systemColor="systemRedColor"/>
</state> </state>
<state key="highlighted"> <state key="highlighted">
@ -179,7 +179,7 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="ZdA-rl-9eP"> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="ZdA-rl-9eP">
<rect key="frame" x="20" y="11" width="334" height="22"/> <rect key="frame" x="20" y="13" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words"/> <textInputTraits key="textInputTraits" autocapitalizationType="words"/>
</textField> </textField>
@ -199,7 +199,7 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notify About New Articles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YV2-gG-lMP"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notify About New Articles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YV2-gG-lMP">
<rect key="frame" x="24" y="11.5" width="197" height="21"/> <rect key="frame" x="24" y="13.5" width="167.5" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -229,7 +229,7 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Always Use Reader View" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bf4-3X-Rfr"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Always Use Reader View" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bf4-3X-Rfr">
<rect key="frame" x="24" y="11.5" width="187" height="21"/> <rect key="frame" x="24" y="13.5" width="158.5" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -256,7 +256,7 @@
<tableViewSection headerTitle="Home Page" id="dTd-6q-SZd"> <tableViewSection headerTitle="Home Page" id="dTd-6q-SZd">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="0zc-o6-Sjh" customClass="VibrantBasicTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="0zc-o6-Sjh" customClass="VibrantBasicTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="204.5" width="374" height="43.5"/> <rect key="frame" x="20" y="198.5" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0zc-o6-Sjh" id="vJs-XK-ebf"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0zc-o6-Sjh" id="vJs-XK-ebf">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
@ -300,7 +300,7 @@
<tableViewSection headerTitle="Feed URL" id="MtQ-oG-lrU"> <tableViewSection headerTitle="Feed URL" id="MtQ-oG-lrU">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fKD-Vi-B8O"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fKD-Vi-B8O">
<rect key="frame" x="20" y="304" width="374" height="43.5"/> <rect key="frame" x="20" y="292" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fKD-Vi-B8O" id="2G0-9f-qwN"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fKD-Vi-B8O" id="2G0-9f-qwN">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>

View File

@ -23,14 +23,8 @@ class WebFeedInspectorViewController: UITableViewController {
@IBOutlet weak var feedURLLabel: InteractiveLabel! @IBOutlet weak var feedURLLabel: InteractiveLabel!
private var headerView: InspectorIconHeaderView? private var headerView: InspectorIconHeaderView?
private var iconImage: IconImage { private var iconImage: IconImage? {
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: webFeed) { return IconImageCache.shared.imageForFeed(webFeed)
return feedIcon
}
if let favicon = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
return favicon
}
return FaviconGenerator.favicon(webFeed)
} }
private let homePageIndexPath = IndexPath(row: 0, section: 1) private let homePageIndexPath = IndexPath(row: 0, section: 1)

View File

@ -85,6 +85,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
super.viewWillAppear(animated) super.viewWillAppear(animated)
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
IconImageCache.shared.emptyCache()
super.traitCollectionDidChange(previousTraitCollection)
reloadAllVisibleCells()
}
// MARK: Notifications // MARK: Notifications
@objc func unreadCountDidChange(_ note: Notification) { @objc func unreadCountDidChange(_ note: Notification) {
@ -842,36 +848,10 @@ private extension MasterFeedViewController {
} }
func configureIcon(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) { func configureIcon(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) {
cell.iconImage = imageFor(identifier) guard let feedID = identifier.feedID else {
return
} }
cell.iconImage = IconImageCache.shared.imageFor(feedID)
func imageFor(_ identifier: MasterFeedTableViewIdentifier) -> IconImage? {
guard let feedID = identifier.feedID else { return nil }
if let smartFeed = SmartFeedsController.shared.find(by: feedID) {
return smartFeed.smallIcon
}
guard let feed = AccountManager.shared.existingFeed(with: feedID) else { return nil }
if let webFeed = feed as? WebFeed {
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed)
if feedIconImage != nil {
return feedIconImage
}
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
return faviconImage
}
}
if let smallIconProvider = feed as? SmallIconProvider {
return smallIconProvider.smallIcon
}
return nil
} }
func nameFor(_ node: Node) -> String { func nameFor(_ node: Node) -> String {
@ -1212,8 +1192,19 @@ private extension MasterFeedViewController {
return nil return nil
} }
var smartFeed: Feed?
if identifier.isPsuedoFeed {
if SmartFeedsController.shared.todayFeed.feedID == identifier.feedID {
smartFeed = SmartFeedsController.shared.todayFeed
} else if SmartFeedsController.shared.unreadFeed.feedID == identifier.feedID {
smartFeed = SmartFeedsController.shared.unreadFeed
} else if SmartFeedsController.shared.starredFeed.feedID == identifier.feedID {
smartFeed = SmartFeedsController.shared.starredFeed
}
}
guard let feedID = identifier.feedID, guard let feedID = identifier.feedID,
let feed = AccountManager.shared.existingFeed(with: feedID), let feed = smartFeed ?? AccountManager.shared.existingFeed(with: feedID),
feed.unreadCount > 0, feed.unreadCount > 0,
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil return nil
@ -1349,13 +1340,12 @@ private extension MasterFeedViewController {
ActivityManager.cleanUp(feed) ActivityManager.cleanUp(feed)
} }
pushUndoableCommand(deleteCommand)
deleteCommand.perform()
if indexPath == coordinator.currentFeedIndexPath { if indexPath == coordinator.currentFeedIndexPath {
coordinator.selectFeed(indexPath: nil) coordinator.selectFeed(indexPath: nil)
} }
pushUndoableCommand(deleteCommand)
deleteCommand.perform()
} }
} }

View File

@ -243,8 +243,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// Set up the read action // Set up the read action
let readTitle = article.status.read ? let readTitle = article.status.read ?
NSLocalizedString("Unread", comment: "Unread") : NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
NSLocalizedString("Read", comment: "Read") NSLocalizedString("Mark as Read", comment: "Mark as Read")
let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completion) in let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completion) in
self?.coordinator.toggleRead(article) self?.coordinator.toggleRead(article)
@ -889,9 +889,7 @@ private extension MasterTimelineViewController {
} }
func openInBrowserAction(_ article: Article) -> UIAction? { func openInBrowserAction(_ article: Article) -> UIAction? {
guard let preferredLink = article.preferredLink, let _ = URL(string: preferredLink) else { guard let _ = article.preferredURL else { return nil }
return nil
}
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in
self?.coordinator.showBrowserForArticle(article) self?.coordinator.showBrowserForArticle(article)
@ -900,9 +898,7 @@ private extension MasterTimelineViewController {
} }
func openInBrowserAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func openInBrowserAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let preferredLink = article.preferredLink, let _ = URL(string: preferredLink) else { guard let _ = article.preferredURL else { return nil }
return nil
}
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.showBrowserForArticle(article) self?.coordinator.showBrowserForArticle(article)
@ -923,10 +919,7 @@ private extension MasterTimelineViewController {
} }
func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? { func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else { guard let url = article.preferredURL else { return nil }
return nil
}
let title = NSLocalizedString("Share", comment: "Share") let title = NSLocalizedString("Share", comment: "Share")
let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title) self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
@ -935,10 +928,7 @@ private extension MasterTimelineViewController {
} }
func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else { guard let url = article.preferredURL else { return nil }
return nil
}
let title = NSLocalizedString("Share", comment: "Share") let title = NSLocalizedString("Share", comment: "Share")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] action in
completion(true) completion(true)

View File

@ -4,17 +4,14 @@
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
\margl1440\margr1440\vieww14220\viewh13280\viewkind0 \margl1440\margr1440\vieww14220\viewh13280\viewkind0
\deftab720 \deftab720
\pard\pardeftab720\li360\fi-360\sa60\partightenfactor0 \pard\pardeftab720\sa60\partightenfactor0
\f0\fs22 \cf2 iOS app design: {\field{\*\fldinst{HYPERLINK "https://inessential.com/"}}{\fldrslt Brent Simmons}} and {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\ \f0\fs22 \cf2 Lead developer: {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\
Lead iOS developer: {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\
App icon: {\field{\*\fldinst{HYPERLINK "https://twitter.com/BradEllis"}}{\fldrslt Brad Ellis}}\ App icon: {\field{\*\fldinst{HYPERLINK "https://twitter.com/BradEllis"}}{\fldrslt Brad Ellis}}\
\pard\pardeftab720\li366\fi-367\sa60\partightenfactor0 Feedly syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/kielgillard"}}{\fldrslt Kiel Gillard}}\
\cf2 Feedly syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/kielgillard"}}{\fldrslt Kiel Gillard}}\ NewsBlur syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/quanganhdo"}}{\fldrslt Anh Do}}\
Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\ Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\
\pard\pardeftab720\li362\fi-363\sa60\partightenfactor0 Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\
\cf2 Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\ Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\
\pard\pardeftab720\li355\fi-356\sa60\partightenfactor0 And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://stuartbreckenridge.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\
\cf2 Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\ }
\pard\pardeftab720\li358\fi-359\sa60\partightenfactor0
\cf2 And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://stuartbreckenridge.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!}

View File

@ -1,9 +1,9 @@
{\rtf1\ansi\ansicpg1252\cocoartf2511 {\rtf1\ansi\ansicpg1252\cocoartf2513
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
\margl1440\margr1440\vieww9000\viewh8400\viewkind0 \margl1440\margr1440\vieww9000\viewh8400\viewkind0
\deftab720 \deftab720
\pard\pardeftab720\li354\fi-355\sa60\partightenfactor0 \pard\pardeftab720\sa60\partightenfactor0
\f0\fs28 \cf2 NetNewsWire 5.0 is dedicated to all the people who showed up to help with code, design, HTML, documentation, icons, testing, and just to help talk things over and think things through. This app\'92s for you!} \f0\fs22 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.}

View File

@ -148,20 +148,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private(set) var currentFeedIndexPath: IndexPath? private(set) var currentFeedIndexPath: IndexPath?
var timelineIconImage: IconImage? { var timelineIconImage: IconImage? {
if let feed = timelineFeed as? WebFeed { guard let timelineFeed = timelineFeed else {
return nil
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: feed)
if feedIconImage != nil {
return feedIconImage
} }
return IconImageCache.shared.imageForFeed(timelineFeed)
if let faviconIconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
return faviconIconImage
}
}
return (timelineFeed as? SmallIconProvider)?.smallIcon
} }
private var exceptionArticleFetcher: ArticleFetcher? private var exceptionArticleFetcher: ArticleFetcher?
@ -853,11 +843,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
currentArticleViewController = articleViewController! currentArticleViewController = articleViewController!
} }
masterTimelineViewController?.updateArticleSelection(animations: animations) // Mark article as read before navigating to it, so the read status does not flash unread/read on display
currentArticleViewController.article = article
markArticles(Set([article!]), statusKey: .read, flag: true) markArticles(Set([article!]), statusKey: .read, flag: true)
masterTimelineViewController?.updateArticleSelection(animations: animations)
currentArticleViewController.article = article
} }
func beginSearching() { func beginSearching() {
@ -1238,16 +1228,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
func showBrowserForArticle(_ article: Article) { func showBrowserForArticle(_ article: Article) {
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else { guard let url = article.preferredURL else { return }
return
}
UIApplication.shared.open(url, options: [:]) UIApplication.shared.open(url, options: [:])
} }
func showBrowserForCurrentArticle() { func showBrowserForCurrentArticle() {
guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else { guard let url = currentArticle?.preferredURL else { return }
return
}
UIApplication.shared.open(url, options: [:]) UIApplication.shared.open(url, options: [:])
} }

View File

@ -28,7 +28,7 @@ class AboutViewController: UITableViewController {
let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0)) let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0))
buildLabel.font = UIFont.systemFont(ofSize: 11.0) buildLabel.font = UIFont.systemFont(ofSize: 11.0)
buildLabel.textColor = UIColor.gray buildLabel.textColor = UIColor.gray
buildLabel.text = NSLocalizedString("Copyright © 2002-2020 Brent Simmons", comment: "Copyright") buildLabel.text = NSLocalizedString("Copyright © 2002-2021 Brent Simmons", comment: "Copyright")
buildLabel.numberOfLines = 0 buildLabel.numberOfLines = 0
buildLabel.sizeToFit() buildLabel.sizeToFit()
buildLabel.translatesAutoresizingMaskIntoConstraints = false buildLabel.translatesAutoresizingMaskIntoConstraints = false

View File

@ -44,7 +44,7 @@ class AddExtensionPointViewController: UITableViewController, AddExtensionPointD
} }
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return NSLocalizedString("Feed Providers allow you to subscribe to web site URL's as if they were RSS feeds.", comment: "Feed Provider Footer") return NSLocalizedString("Feed Providers allow you to subscribe to some pages as if they were RSS feeds.", comment: "Feed Provider Footer")
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

View File

@ -1,7 +1,7 @@
// High Level Settings common to both the iOS application and any extensions we bundle with it // High Level Settings common to both the iOS application and any extensions we bundle with it
MARKETING_VERSION = 6.0 MARKETING_VERSION = 6.0
CURRENT_PROJECT_VERSION = 601 CURRENT_PROJECT_VERSION = 603
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon