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: [
.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/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: "../Articles", .upToNextMajor(from: "1.0.0")),
.package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")),

View File

@ -180,7 +180,11 @@ final class CloudKitAccountZone: CloudKitZone {
}
case .failure(let error):
completion(.failure(error))
if let ckError = ((error as? CloudKitError)?.error as? CKError), ckError.code == .unknownItem {
completion(.success(true))
} else {
completion(.failure(error))
}
}
}
}

View File

@ -16,8 +16,9 @@ import Articles
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
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")
weak var account: Account?
@ -75,7 +76,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
if let container = account.existingContainer(withExternalID: containerExternalID) {
createWebFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, container: container)
} 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
}
if let folder = folder, let containerExternalID = folder.externalID, let unclaimedWebFeeds = unclaimedWebFeeds[containerExternalID] {
for unclaimedWebFeed in unclaimedWebFeeds {
createWebFeedIfNecessary(url: unclaimedWebFeed.url,
name: unclaimedWebFeed.name,
editedName: unclaimedWebFeed.editedName,
homePageURL: unclaimedWebFeed.homePageURL,
webFeedExternalID: unclaimedWebFeed.webFeedExternalID,
container: folder)
guard let container = folder, let containerExternalID = container.externalID else { return }
if let newUnclaimedWebFeeds = newUnclaimedWebFeeds[containerExternalID] {
for newUnclaimedWebFeed in newUnclaimedWebFeeds {
createWebFeedIfNecessary(url: newUnclaimedWebFeed.url,
name: newUnclaimedWebFeed.name,
editedName: newUnclaimedWebFeed.editedName,
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) {
@ -152,6 +161,8 @@ private extension CloudKitAcountZoneDelegate {
case .insert(_, let externalID, _):
if let container = account.existingContainer(withExternalID: externalID) {
container.addWebFeed(webFeed)
} else {
addExistingUnclaimedWebFeed(webFeed, containerExternalID: externalID)
}
}
}
@ -170,14 +181,25 @@ private extension CloudKitAcountZoneDelegate {
container.addWebFeed(webFeed)
}
func addUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) {
if var unclaimedWebFeeds = self.unclaimedWebFeeds[containerExternalID] {
func addNewUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) {
if var unclaimedWebFeeds = self.newUnclaimedWebFeeds[containerExternalID] {
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
self.unclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
} else {
var unclaimedWebFeeds = [UnclaimedWebFeed]()
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 prevIndex = displayStartIndex
var emojiOffset = 0
var unicodeScalarOffset = 0
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.
let emojiEndIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex
if prevIndex < emojiEndIndex {
let emojis = String(text[prevIndex..<emojiEndIndex]).emojis
for emoji in emojis {
emojiOffset += emoji.unicodeScalars.count - 1
// The twitter indices are messed up by characters with more than one scalar, we are going to adjust for that here.
let endIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex
if prevIndex < endIndex {
let characters = String(text[prevIndex..<endIndex])
for character in characters {
unicodeScalarOffset += character.unicodeScalars.count - 1
}
}
let offsetStartIndex = entity.startIndex - emojiOffset
let offsetEndIndex = entity.endIndex - emojiOffset
let offsetStartIndex = entity.startIndex - unicodeScalarOffset
let offsetEndIndex = entity.endIndex - unicodeScalarOffset
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

View File

@ -10,7 +10,7 @@ import Foundation
import RSParser
import RSCore
final class FeedbinEntry: Codable {
final class FeedbinEntry: Decodable {
let articleID: Int
let feedID: Int
@ -50,14 +50,25 @@ final class FeedbinEntry: Codable {
}
}
struct FeedbinEntryJSONFeed: Codable {
struct FeedbinEntryJSONFeed: Decodable {
let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor?
enum CodingKeys: String, CodingKey {
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 avatarURL: String?
enum CodingKeys: String, CodingKey {

View File

@ -310,7 +310,7 @@ final class FeedlyAPICaller {
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
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 {
case .success(let (httpResponse, _)):
if httpResponse.statusCode == 200 {

View File

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

View File

@ -14,10 +14,24 @@ import SyncDatabase
import os.log
import Secrets
public enum ReaderAPIAccountDelegateError: String, Error {
case unknown = "An unknown error occurred."
case invalidParameter = "There was an invalid parameter passed."
case invalidResponse = "There was an invalid response from the server."
public enum ReaderAPIAccountDelegateError: LocalizedError {
case unknown
case invalidParameter
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 {

View File

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

View File

@ -15,7 +15,7 @@ let package = Package(
dependencies: [
.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/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")),
],
targets: [

View File

@ -149,18 +149,7 @@ private extension WebFeedInspectorViewController {
guard let feed = feed, let iconView = iconView else {
return
}
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
iconView.iconImage = IconImageCache.shared.imageForFeed(feed)
}
func updateName() {

View File

@ -768,7 +768,7 @@ private extension SidebarViewController {
}
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
}
if let smallIconProvider = node.representedObject as? SmallIconProvider {

View File

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

View File

@ -886,28 +886,7 @@ extension TimelineViewController: NSTableViewDelegate {
if !showIcons {
return nil
}
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)
return IconImageCache.shared.imageForArticle(article)
}
private func avatarForAuthor(_ author: Author) -> IconImage? {

View File

@ -107,7 +107,7 @@ struct ArticleToolbarModifier: ViewModifier {
.disabled(sceneModel.shareButtonState == nil)
.help("Share")
.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)
}
}

View File

@ -41,20 +41,6 @@ final class FeedIconImageLoader: ObservableObject {
private extension FeedIconImageLoader {
func fetchImage() {
if let webFeed = feed as? WebFeed {
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
}
image = IconImageCache.shared.imageForFeed(feed)
}
}

View File

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

View File

@ -1,9 +1,9 @@
{\rtf1\ansi\ansicpg1252\cocoartf2511
{\rtf1\ansi\ansicpg1252\cocoartf2513
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
\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")) {
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)
}
}.listStyle(InsetGroupedListStyle())

View File

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

View File

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

View File

@ -157,7 +157,7 @@
document.addEventListener("click", (ev) =>
{
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);
if (!id) return;
const fnref = document.getElementById(id);

View File

@ -382,7 +382,8 @@ img[src*="share-buttons"] {
.newsfoot-footnote-popover .reversefootnote,
.newsfoot-footnote-popover .footnoteBackLink,
.newsfoot-footnote-popover .footnote-return {
.newsfoot-footnote-popover .footnote-return,
.newsfoot-footnote-popover a[href*='#fn'] {
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.
let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag)
if articlesToMark.isEmpty {
completion?()
return nil
}
self.articles = Set(articlesToMark)

View File

@ -56,6 +56,18 @@ extension Article {
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? {
return contentHTML ?? contentText ?? summary
}
@ -84,32 +96,7 @@ extension Article {
}
func iconImage() -> IconImage? {
if let authors = authors, authors.count == 1, let author = authors.first {
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)
return IconImageCache.shared.imageForArticle(self)
}
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] {
return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in
if let feedID = feed.feedID, !filterExceptions.contains(feedID) && isReadFiltered && feed.unreadCount == 0 {
return nil
}
// All Smart Feeds should remain visible despite the Hide Read Feeds setting
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 {
case .freshRSS:
title = NSLocalizedString("FreshRSS", comment: "FreshRSS")
apiURLTextField.placeholder = NSLocalizedString("API URL: fresh.rss.net/api/greader.php", comment: "FreshRSS API Helper")
case .inoreader:
title = NSLocalizedString("InoReader", comment: "InoReader")
case .bazQux:

View File

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

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
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
@ -235,20 +242,14 @@ class WebViewController: UIViewController {
}
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
return
}
guard let url = article?.preferredURL else { return }
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()])
activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem
present(activityViewController, animated: true)
}
func openInAppBrowser() {
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
return
}
guard let url = article?.preferredURL else { return }
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}

View File

@ -12,33 +12,24 @@ final class IconView: UIView {
var iconImage: IconImage? = nil {
didSet {
if iconImage !== oldValue {
imageView.image = iconImage?.image
if self.traitCollection.userInterfaceStyle == .dark {
if self.iconImage?.isDark ?? false {
self.isDiscernable = false
self.setNeedsLayout()
} else {
self.isDiscernable = true
self.setNeedsLayout()
}
} else {
if self.iconImage?.isBright ?? false {
self.isDiscernable = false
self.setNeedsLayout()
} else {
self.isDiscernable = true
self.setNeedsLayout()
}
}
self.setNeedsLayout()
guard iconImage !== oldValue else {
return
}
imageView.image = iconImage?.image
if traitCollection.userInterfaceStyle == .dark {
let isDark = iconImage?.isDark ?? false
isDiscernable = !isDark
}
else {
let isBright = iconImage?.isBright ?? false
isDiscernable = !isBright
}
setNeedsLayout()
}
}
private var isDiscernable = true
private let imageView: UIImageView = {
let imageView = NonIntrinsicImageView(image: AppAssets.faviconTemplateImage)
imageView.contentMode = .scaleAspectFit
@ -79,13 +70,8 @@ final class IconView: UIView {
override func layoutSubviews() {
imageView.setFrameIfNotEqual(rectForImageView())
if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) {
backgroundColor = AppAssets.iconBackgroundColor
} else {
backgroundColor = nil
}
updateBackgroundColor()
}
}
private extension IconView {
@ -125,4 +111,11 @@ private extension IconView {
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
}
let title = NSLocalizedString("Delete Account", comment: "Delete Account")
let title = NSLocalizedString("Remove Account", comment: "Remove Account")
let message: String = {
switch account.type {
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:
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)
@ -106,7 +106,7 @@ class AccountInspectorViewController: UITableViewController {
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
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
guard let self = self, let account = self.account else { return }
AccountManager.shared.deleteAccount(account)

View File

@ -1,9 +1,9 @@
<?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"/>
<dependencies>
<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="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -27,10 +27,10 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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>
<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"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
</textField>
@ -52,7 +52,7 @@
<autoresizingMask key="autoresizingMask"/>
<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">
<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"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -119,7 +119,7 @@
<constraint firstAttribute="height" constant="44" id="WtN-fp-Ldt"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Delete Account">
<state key="normal" title="Remove Account">
<color key="titleColor" systemColor="systemRedColor"/>
</state>
<state key="highlighted">
@ -179,7 +179,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
</textField>
@ -199,7 +199,7 @@
<autoresizingMask key="autoresizingMask"/>
<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">
<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"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -229,7 +229,7 @@
<autoresizingMask key="autoresizingMask"/>
<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">
<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"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -256,7 +256,7 @@
<tableViewSection headerTitle="Home Page" id="dTd-6q-SZd">
<cells>
<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"/>
<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"/>
@ -300,7 +300,7 @@
<tableViewSection headerTitle="Feed URL" id="MtQ-oG-lrU">
<cells>
<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"/>
<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"/>

View File

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

View File

@ -41,7 +41,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
@ -85,6 +85,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
super.viewWillAppear(animated)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
IconImageCache.shared.emptyCache()
super.traitCollectionDidChange(previousTraitCollection)
reloadAllVisibleCells()
}
// MARK: Notifications
@objc func unreadCountDidChange(_ note: Notification) {
@ -842,38 +848,12 @@ private extension MasterFeedViewController {
}
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 {
if let displayNameProvider = node.representedObject as? DisplayNameProvider {
return displayNameProvider.nameForDisplay
@ -1211,9 +1191,20 @@ private extension MasterFeedViewController {
guard let identifier = dataSource.itemIdentifier(for: indexPath), identifier.unreadCount > 0 else {
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,
let feed = AccountManager.shared.existingFeed(with: feedID),
let feed = smartFeed ?? AccountManager.shared.existingFeed(with: feedID),
feed.unreadCount > 0,
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil
@ -1349,13 +1340,12 @@ private extension MasterFeedViewController {
ActivityManager.cleanUp(feed)
}
pushUndoableCommand(deleteCommand)
deleteCommand.perform()
if indexPath == coordinator.currentFeedIndexPath {
coordinator.selectFeed(indexPath: nil)
}
pushUndoableCommand(deleteCommand)
deleteCommand.perform()
}
}

View File

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

View File

@ -4,17 +4,14 @@
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
\margl1440\margr1440\vieww14220\viewh13280\viewkind0
\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}}\
Lead iOS developer: {\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}}\
App icon: {\field{\*\fldinst{HYPERLINK "https://twitter.com/BradEllis"}}{\fldrslt Brad Ellis}}\
\pard\pardeftab720\li366\fi-367\sa60\partightenfactor0
\cf2 Feedly syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/kielgillard"}}{\fldrslt Kiel Gillard}}\
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}}\
\pard\pardeftab720\li362\fi-363\sa60\partightenfactor0
\cf2 Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\
\pard\pardeftab720\li355\fi-356\sa60\partightenfactor0
\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}}!}
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}}\
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}}!\
}

View File

@ -1,9 +1,9 @@
{\rtf1\ansi\ansicpg1252\cocoartf2511
{\rtf1\ansi\ansicpg1252\cocoartf2513
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
\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

@ -146,22 +146,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
// At some point we should refactor the current Feed IndexPath out and only use the timeline feed
private(set) var currentFeedIndexPath: IndexPath?
var timelineIconImage: IconImage? {
if let feed = timelineFeed as? WebFeed {
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: feed)
if feedIconImage != nil {
return feedIconImage
}
if let faviconIconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
return faviconIconImage
}
guard let timelineFeed = timelineFeed else {
return nil
}
return (timelineFeed as? SmallIconProvider)?.smallIcon
return IconImageCache.shared.imageForFeed(timelineFeed)
}
private var exceptionArticleFetcher: ArticleFetcher?
@ -853,11 +843,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
currentArticleViewController = articleViewController!
}
// Mark article as read before navigating to it, so the read status does not flash unread/read on display
markArticles(Set([article!]), statusKey: .read, flag: true)
masterTimelineViewController?.updateArticleSelection(animations: animations)
currentArticleViewController.article = article
markArticles(Set([article!]), statusKey: .read, flag: true)
}
func beginSearching() {
@ -1238,16 +1228,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func showBrowserForArticle(_ article: Article) {
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
return
}
guard let url = article.preferredURL else { return }
UIApplication.shared.open(url, options: [:])
}
func showBrowserForCurrentArticle() {
guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
return
}
guard let url = currentArticle?.preferredURL else { return }
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))
buildLabel.font = UIFont.systemFont(ofSize: 11.0)
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.sizeToFit()
buildLabel.translatesAutoresizingMaskIntoConstraints = false

View File

@ -44,7 +44,7 @@ class AddExtensionPointViewController: UITableViewController, AddExtensionPointD
}
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) {

View File

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