Rename Feed protocol to SidebarItem. Rename FeedIdentifier to SidebarItemIdentifier. Rename WebFeed to Feed.

This commit is contained in:
Brent Simmons 2024-11-01 21:34:08 -07:00
parent 0912bfed18
commit 050c47c41d
65 changed files with 753 additions and 753 deletions

View File

@ -57,7 +57,7 @@ public enum FetchType {
case unread(_: Int? = nil) case unread(_: Int? = nil)
case today(_: Int? = nil) case today(_: Int? = nil)
case folder(Folder, Bool) case folder(Folder, Bool)
case webFeed(WebFeed) case webFeed(Feed)
case articleIDs(Set<String>) case articleIDs(Set<String>)
case search(String) case search(String)
case searchWithArticleIDs(String, Set<String>) case searchWithArticleIDs(String, Set<String>)
@ -143,7 +143,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
} }
public var topLevelWebFeeds = Set<WebFeed>() public var topLevelWebFeeds = Set<Feed>()
public var folders: Set<Folder>? = Set<Folder>() public var folders: Set<Folder>? = Set<Folder>()
public var externalID: String? { public var externalID: String? {
@ -163,15 +163,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
private var webFeedDictionariesNeedUpdate = true private var webFeedDictionariesNeedUpdate = true
private var _idToWebFeedDictionary = [String: WebFeed]() private var _idToWebFeedDictionary = [String: Feed]()
var idToWebFeedDictionary: [String: WebFeed] { var idToWebFeedDictionary: [String: Feed] {
if webFeedDictionariesNeedUpdate { if webFeedDictionariesNeedUpdate {
rebuildWebFeedDictionaries() rebuildWebFeedDictionaries()
} }
return _idToWebFeedDictionary return _idToWebFeedDictionary
} }
private var _externalIDToWebFeedDictionary = [String: WebFeed]() private var _externalIDToWebFeedDictionary = [String: Feed]()
var externalIDToWebFeedDictionary: [String: WebFeed] { var externalIDToWebFeedDictionary: [String: Feed] {
if webFeedDictionariesNeedUpdate { if webFeedDictionariesNeedUpdate {
rebuildWebFeedDictionaries() rebuildWebFeedDictionaries()
} }
@ -214,7 +214,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
private var unreadCounts = [String: Int]() // [feedID: Int] private var unreadCounts = [String: Int]() // [feedID: Int]
private var _flattenedWebFeeds = Set<WebFeed>() private var _flattenedWebFeeds = Set<Feed>()
private var flattenedWebFeedsNeedUpdate = true private var flattenedWebFeedsNeedUpdate = true
private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self) private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self)
@ -535,7 +535,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return existingFolder(withExternalID: externalID) return existingFolder(withExternalID: externalID)
} }
func existingContainers(withWebFeed webFeed: WebFeed) -> [Container] { func existingContainers(withWebFeed webFeed: Feed) -> [Container] {
var containers = [Container]() var containers = [Container]()
if topLevelWebFeeds.contains(webFeed) { if topLevelWebFeeds.contains(webFeed) {
containers.append(self) containers.append(self)
@ -586,10 +586,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return folders?.first(where: { $0.externalID == externalID }) return folders?.first(where: { $0.externalID == externalID })
} }
func newWebFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> WebFeed { func newWebFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
let feedURL = opmlFeedSpecifier.feedURL let feedURL = opmlFeedSpecifier.feedURL
let metadata = webFeedMetadata(feedURL: feedURL, webFeedID: feedURL) let metadata = webFeedMetadata(feedURL: feedURL, webFeedID: feedURL)
let feed = WebFeed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata) let feed = Feed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata)
if let feedTitle = opmlFeedSpecifier.title { if let feedTitle = opmlFeedSpecifier.title {
if feed.name == nil { if feed.name == nil {
feed.name = feedTitle feed.name = feedTitle
@ -598,35 +598,35 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return feed return feed
} }
public func addWebFeed(_ feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) { public func addWebFeed(_ feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
delegate.addWebFeed(for: self, with: feed, to: container, completion: completion) delegate.addWebFeed(for: self, with: feed, to: container, completion: completion)
} }
public func createWebFeed(url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { public func createWebFeed(url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
delegate.createWebFeed(for: self, url: url, name: name, container: container, validateFeed: validateFeed, completion: completion) delegate.createWebFeed(for: self, url: url, name: name, container: container, validateFeed: validateFeed, completion: completion)
} }
func createWebFeed(with name: String?, url: String, webFeedID: String, homePageURL: String?) -> WebFeed { func createWebFeed(with name: String?, url: String, webFeedID: String, homePageURL: String?) -> Feed {
let metadata = webFeedMetadata(feedURL: url, webFeedID: webFeedID) let metadata = webFeedMetadata(feedURL: url, webFeedID: webFeedID)
let feed = WebFeed(account: self, url: url, metadata: metadata) let feed = Feed(account: self, url: url, metadata: metadata)
feed.name = name feed.name = name
feed.homePageURL = homePageURL feed.homePageURL = homePageURL
return feed return feed
} }
public func removeWebFeed(_ feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { public func removeWebFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
delegate.removeWebFeed(for: self, with: feed, from: container, completion: completion) delegate.removeWebFeed(for: self, with: feed, from: container, completion: completion)
} }
public func moveWebFeed(_ feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { public func moveWebFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
delegate.moveWebFeed(for: self, with: feed, from: from, to: to, completion: completion) delegate.moveWebFeed(for: self, with: feed, from: from, to: to, completion: completion)
} }
public func renameWebFeed(_ feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) { public func renameWebFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
delegate.renameWebFeed(for: self, with: feed, to: name, completion: completion) delegate.renameWebFeed(for: self, with: feed, to: name, completion: completion)
} }
public func restoreWebFeed(_ feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) { public func restoreWebFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
delegate.restoreWebFeed(for: self, feed: feed, container: container, completion: completion) delegate.restoreWebFeed(for: self, feed: feed, container: container, completion: completion)
} }
@ -646,7 +646,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
delegate.restoreFolder(for: self, folder: folder, completion: completion) delegate.restoreFolder(for: self, folder: folder, completion: completion)
} }
func clearWebFeedMetadata(_ feed: WebFeed) { func clearWebFeedMetadata(_ feed: Feed) {
webFeedMetadata[feed.url] = nil webFeedMetadata[feed.url] = nil
} }
@ -656,7 +656,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
structureDidChange() structureDidChange()
} }
public func updateUnreadCounts(for webFeeds: Set<WebFeed>, completion: VoidCompletionBlock? = nil) { public func updateUnreadCounts(for webFeeds: Set<Feed>, completion: VoidCompletionBlock? = nil) {
fetchUnreadCounts(for: webFeeds, completion: completion) fetchUnreadCounts(for: webFeeds, completion: completion)
} }
@ -735,11 +735,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion) database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
} }
public func unreadCount(for webFeed: WebFeed) -> Int { public func unreadCount(for webFeed: Feed) -> Int {
return unreadCounts[webFeed.webFeedID] ?? 0 return unreadCounts[webFeed.webFeedID] ?? 0
} }
public func setUnreadCount(_ unreadCount: Int, for webFeed: WebFeed) { public func setUnreadCount(_ unreadCount: Int, for webFeed: Feed) {
unreadCounts[webFeed.webFeedID] = unreadCount unreadCounts[webFeed.webFeedID] = unreadCount
} }
@ -751,7 +751,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
webFeedDictionariesNeedUpdate = true webFeedDictionariesNeedUpdate = true
} }
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) { func update(_ webFeed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) {
// Used only by an On My Mac or iCloud account. // Used only by an On My Mac or iCloud account.
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
precondition(type == .onMyMac || type == .cloudKit) precondition(type == .onMyMac || type == .cloudKit)
@ -899,7 +899,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
// MARK: - Container // MARK: - Container
public func flattenedWebFeeds() -> Set<WebFeed> { public func flattenedWebFeeds() -> Set<Feed> {
assert(Thread.isMainThread) assert(Thread.isMainThread)
if flattenedWebFeedsNeedUpdate { if flattenedWebFeedsNeedUpdate {
updateFlattenedWebFeeds() updateFlattenedWebFeeds()
@ -907,13 +907,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return _flattenedWebFeeds return _flattenedWebFeeds
} }
public func removeWebFeed(_ webFeed: WebFeed) { public func removeWebFeed(_ webFeed: Feed) {
topLevelWebFeeds.remove(webFeed) topLevelWebFeeds.remove(webFeed)
structureDidChange() structureDidChange()
postChildrenDidChangeNotification() postChildrenDidChangeNotification()
} }
public func removeFeeds(_ webFeeds: Set<WebFeed>) { public func removeFeeds(_ webFeeds: Set<Feed>) {
guard !webFeeds.isEmpty else { guard !webFeeds.isEmpty else {
return return
} }
@ -922,13 +922,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
postChildrenDidChangeNotification() postChildrenDidChangeNotification()
} }
public func addWebFeed(_ webFeed: WebFeed) { public func addWebFeed(_ webFeed: Feed) {
topLevelWebFeeds.insert(webFeed) topLevelWebFeeds.insert(webFeed)
structureDidChange() structureDidChange()
postChildrenDidChangeNotification() postChildrenDidChangeNotification()
} }
func addFeedIfNotInAnyFolder(_ webFeed: WebFeed) { func addFeedIfNotInAnyFolder(_ webFeed: Feed) {
if !flattenedWebFeeds().contains(webFeed) { if !flattenedWebFeeds().contains(webFeed) {
addWebFeed(webFeed) addWebFeed(webFeed)
} }
@ -970,7 +970,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
@objc func unreadCountDidChange(_ note: Notification) { @objc func unreadCountDidChange(_ note: Notification) {
if let feed = note.object as? WebFeed, feed.account === self { if let feed = note.object as? Feed, feed.account === self {
updateUnreadCount() updateUnreadCount()
} }
} }
@ -1078,13 +1078,13 @@ private extension Account {
fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion) fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion)
} }
func fetchArticles(webFeed: WebFeed) throws -> Set<Article> { func fetchArticles(webFeed: Feed) throws -> Set<Article> {
let articles = try database.fetchArticles(webFeed.webFeedID) let articles = try database.fetchArticles(webFeed.webFeedID)
validateUnreadCount(webFeed, articles) validateUnreadCount(webFeed, articles)
return articles return articles
} }
func fetchArticlesAsync(webFeed: WebFeed, _ completion: @escaping ArticleSetResultBlock) { func fetchArticlesAsync(webFeed: Feed, _ completion: @escaping ArticleSetResultBlock) {
database.fetchArticlesAsync(webFeed.webFeedID) { [weak self] articleSetResult in database.fetchArticlesAsync(webFeed.webFeedID) { [weak self] articleSetResult in
switch articleSetResult { switch articleSetResult {
case .success(let articles): case .success(let articles):
@ -1120,7 +1120,7 @@ private extension Account {
return database.fetchArticlesAsync(articleIDs: articleIDs, completion) return database.fetchArticlesAsync(articleIDs: articleIDs, completion)
} }
func fetchUnreadArticles(webFeed: WebFeed) throws -> Set<Article> { func fetchUnreadArticles(webFeed: Feed) throws -> Set<Article> {
let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID]), nil) let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID]), nil)
validateUnreadCount(webFeed, articles) validateUnreadCount(webFeed, articles)
return articles return articles
@ -1178,7 +1178,7 @@ private extension Account {
} }
} }
func validateUnreadCountsAfterFetchingUnreadArticles(_ webFeeds: Set<WebFeed>, _ articles: Set<Article>) { func validateUnreadCountsAfterFetchingUnreadArticles(_ webFeeds: Set<Feed>, _ articles: Set<Article>) {
// Validate unread counts. This was the site of a performance slowdown: // Validate unread counts. This was the site of a performance slowdown:
// it was calling going through the entire list of articles once per feed: // it was calling going through the entire list of articles once per feed:
// feeds.forEach { validateUnreadCount($0, articles) } // feeds.forEach { validateUnreadCount($0, articles) }
@ -1194,7 +1194,7 @@ private extension Account {
} }
} }
func validateUnreadCount(_ webFeed: WebFeed, _ articles: Set<Article>) { func validateUnreadCount(_ webFeed: Feed, _ articles: Set<Article>) {
// articles must contain all the unread articles for the feed. // articles must contain all the unread articles for the feed.
// The unread number should match the feeds unread count. // The unread number should match the feeds unread count.
@ -1225,7 +1225,7 @@ private extension Account {
} }
func updateFlattenedWebFeeds() { func updateFlattenedWebFeeds() {
var feeds = Set<WebFeed>() var feeds = Set<Feed>()
feeds.formUnion(topLevelWebFeeds) feeds.formUnion(topLevelWebFeeds)
for folder in folders! { for folder in folders! {
feeds.formUnion(folder.flattenedWebFeeds()) feeds.formUnion(folder.flattenedWebFeeds())
@ -1236,8 +1236,8 @@ private extension Account {
} }
func rebuildWebFeedDictionaries() { func rebuildWebFeedDictionaries() {
var idDictionary = [String: WebFeed]() var idDictionary = [String: Feed]()
var externalIDDictionary = [String: WebFeed]() var externalIDDictionary = [String: Feed]()
flattenedWebFeeds().forEach { (feed) in flattenedWebFeeds().forEach { (feed) in
idDictionary[feed.webFeedID] = feed idDictionary[feed.webFeedID] = feed
@ -1287,7 +1287,7 @@ private extension Account {
/// Fetch unread counts for zero or more feeds. /// Fetch unread counts for zero or more feeds.
/// ///
/// Uses the most efficient method based on how many feeds were passed in. /// Uses the most efficient method based on how many feeds were passed in.
func fetchUnreadCounts(for feeds: Set<WebFeed>, completion: VoidCompletionBlock?) { func fetchUnreadCounts(for feeds: Set<Feed>, completion: VoidCompletionBlock?) {
if feeds.isEmpty { if feeds.isEmpty {
completion?() completion?()
return return
@ -1303,7 +1303,7 @@ private extension Account {
} }
} }
func fetchUnreadCount(_ feed: WebFeed, _ completion: VoidCompletionBlock?) { func fetchUnreadCount(_ feed: Feed, _ completion: VoidCompletionBlock?) {
database.fetchUnreadCount(feed.webFeedID) { result in database.fetchUnreadCount(feed.webFeedID) { result in
if let unreadCount = try? result.get() { if let unreadCount = try? result.get() {
feed.unreadCount = unreadCount feed.unreadCount = unreadCount
@ -1312,7 +1312,7 @@ private extension Account {
} }
} }
func fetchUnreadCounts(_ feeds: Set<WebFeed>, _ completion: VoidCompletionBlock?) { func fetchUnreadCounts(_ feeds: Set<Feed>, _ completion: VoidCompletionBlock?) {
let webFeedIDs = Set(feeds.map { $0.webFeedID }) let webFeedIDs = Set(feeds.map { $0.webFeedID })
database.fetchUnreadCounts(for: webFeedIDs) { result in database.fetchUnreadCounts(for: webFeedIDs) { result in
if let unreadCountDictionary = try? result.get() { if let unreadCountDictionary = try? result.get() {
@ -1342,7 +1342,7 @@ private extension Account {
} }
} }
func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set<WebFeed>) { func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set<Feed>) {
for feed in feeds { for feed in feeds {
// When the unread count is zero, it wont appear in unreadCountDictionary. // When the unread count is zero, it wont appear in unreadCountDictionary.
let unreadCount = unreadCountDictionary[feed.webFeedID] ?? 0 let unreadCount = unreadCountDictionary[feed.webFeedID] ?? 0
@ -1351,7 +1351,7 @@ private extension Account {
} }
func sendNotificationAbout(_ articleChanges: ArticleChanges) { func sendNotificationAbout(_ articleChanges: ArticleChanges) {
var webFeeds = Set<WebFeed>() var webFeeds = Set<Feed>()
if let newArticles = articleChanges.newArticles { if let newArticles = articleChanges.newArticles {
webFeeds.formUnion(Set(newArticles.compactMap { $0.webFeed })) webFeeds.formUnion(Set(newArticles.compactMap { $0.webFeed }))
@ -1394,11 +1394,11 @@ private extension Account {
extension Account { extension Account {
public func existingWebFeed(withWebFeedID webFeedID: String) -> WebFeed? { public func existingWebFeed(withWebFeedID webFeedID: String) -> Feed? {
return idToWebFeedDictionary[webFeedID] return idToWebFeedDictionary[webFeedID]
} }
public func existingWebFeed(withExternalID externalID: String) -> WebFeed? { public func existingWebFeed(withExternalID externalID: String) -> Feed? {
return externalIDToWebFeedDictionary[externalID] return externalIDToWebFeedDictionary[externalID]
} }

View File

@ -36,13 +36,13 @@ protocol AccountDelegate {
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void)
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
func addWebFeed(for account: Account, with: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) func addWebFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void)
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void) func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void)

View File

@ -197,7 +197,7 @@ public final class AccountManager: UnreadCountProvider {
return nil return nil
} }
public func existingFeed(with feedID: FeedIdentifier) -> Feed? { public func existingFeed(with feedID: SidebarItemIdentifier) -> SidebarItem? {
switch feedID { switch feedID {
case .folder(let accountID, let folderName): case .folder(let accountID, let folderName):
if let account = existingAccount(with: accountID) { if let account = existingAccount(with: accountID) {

View File

@ -18,7 +18,7 @@ public protocol ArticleFetcher {
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
} }
extension WebFeed: ArticleFetcher { extension Feed: ArticleFetcher {
public func fetchArticles() throws -> Set<Article> { public func fetchArticles() throws -> Set<Article> {
return try account?.fetchArticles(.webFeed(self)) ?? Set<Article>() return try account?.fetchArticles(.webFeed(self)) ?? Set<Article>()

View File

@ -175,7 +175,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
} }
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
completion(.failure(LocalAccountDelegateError.invalidParameter)) completion(.failure(LocalAccountDelegateError.invalidParameter))
return return
@ -186,7 +186,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, validateFeed: validateFeed, completion: completion) createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, validateFeed: validateFeed, completion: completion)
} }
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) { func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
let editedName = name.isEmpty ? nil : name let editedName = name.isEmpty ? nil : name
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
accountZone.renameWebFeed(feed, editedName: editedName) { result in accountZone.renameWebFeed(feed, editedName: editedName) { result in
@ -202,7 +202,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
} }
} }
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
removeWebFeedFromCloud(for: account, with: feed, from: container) { result in removeWebFeedFromCloud(for: account, with: feed, from: container) { result in
switch result { switch result {
case .success: case .success:
@ -222,7 +222,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
} }
} }
func moveWebFeed(for account: Account, with feed: WebFeed, from fromContainer: Container, to toContainer: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(for account: Account, with feed: Feed, from fromContainer: Container, to toContainer: Container, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
accountZone.moveWebFeed(feed, from: fromContainer, to: toContainer) { result in accountZone.moveWebFeed(feed, from: fromContainer, to: toContainer) { result in
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
@ -238,7 +238,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
} }
} }
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
accountZone.addWebFeed(feed, to: container) { result in accountZone.addWebFeed(feed, to: container) { result in
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
@ -253,7 +253,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
} }
} }
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
switch result { switch result {
case .success: case .success:
@ -562,7 +562,7 @@ private extension CloudKitAccountDelegate {
} }
func combinedRefresh(_ account: Account, _ webFeeds: Set<WebFeed>, completion: @escaping (Result<Void, Error>) -> Void) { func combinedRefresh(_ account: Account, _ webFeeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
let group = DispatchGroup() let group = DispatchGroup()
@ -576,7 +576,7 @@ private extension CloudKitAccountDelegate {
} }
} }
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
func addDeadFeed() { func addDeadFeed() {
let feed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil) let feed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
@ -683,7 +683,7 @@ private extension CloudKitAccountDelegate {
} }
} }
func sendNewArticlesToTheCloud(_ account: Account, _ feed: WebFeed) { func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
account.fetchArticlesAsync(.webFeed(feed)) { result in account.fetchArticlesAsync(.webFeed(feed)) { result in
switch result { switch result {
case .success(let articles): case .success(let articles):
@ -771,7 +771,7 @@ private extension CloudKitAccountDelegate {
} }
func removeWebFeedFromCloud(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeWebFeedFromCloud(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(2) refreshProgress.addToNumberOfTasksAndRemaining(2)
accountZone.removeWebFeed(feed, from: container) { result in accountZone.removeWebFeed(feed, from: container) { result in
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
@ -798,7 +798,7 @@ private extension CloudKitAccountDelegate {
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate { extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) { func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed) {
refreshProgress.completeTask() refreshProgress.completeTask()
} }

View File

@ -119,7 +119,7 @@ final class CloudKitAccountZone: CloudKitZone {
} }
/// Rename the given web feed /// Rename the given web feed
func renameWebFeed(_ webFeed: WebFeed, editedName: String?, completion: @escaping (Result<Void, Error>) -> Void) { func renameWebFeed(_ webFeed: Feed, editedName: String?, completion: @escaping (Result<Void, Error>) -> Void) {
guard let externalID = webFeed.externalID else { guard let externalID = webFeed.externalID else {
completion(.failure(CloudKitZoneError.corruptAccount)) completion(.failure(CloudKitZoneError.corruptAccount))
return return
@ -140,7 +140,7 @@ final class CloudKitAccountZone: CloudKitZone {
} }
/// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted /// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted
func removeWebFeed(_ webFeed: WebFeed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) { func removeWebFeed(_ webFeed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
guard let fromContainerExternalID = from.externalID else { guard let fromContainerExternalID = from.externalID else {
completion(.failure(CloudKitZoneError.corruptAccount)) completion(.failure(CloudKitZoneError.corruptAccount))
return return
@ -189,7 +189,7 @@ final class CloudKitAccountZone: CloudKitZone {
} }
} }
func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(_ webFeed: Feed, 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 {
completion(.failure(CloudKitZoneError.corruptAccount)) completion(.failure(CloudKitZoneError.corruptAccount))
return return
@ -211,7 +211,7 @@ final class CloudKitAccountZone: CloudKitZone {
} }
} }
func addWebFeed(_ webFeed: WebFeed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func addWebFeed(_ webFeed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let toContainerExternalID = to.externalID else { guard let toContainerExternalID = to.externalID else {
completion(.failure(CloudKitZoneError.corruptAccount)) completion(.failure(CloudKitZoneError.corruptAccount))
return return

View File

@ -17,7 +17,7 @@ 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 newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]() private var newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]()
private var existingUnclaimedWebFeeds = [String: [WebFeed]]() private var existingUnclaimedWebFeeds = [String: [Feed]]()
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
@ -140,7 +140,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
private extension CloudKitAcountZoneDelegate { private extension CloudKitAcountZoneDelegate {
func updateWebFeed(_ webFeed: WebFeed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) { func updateWebFeed(_ webFeed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
guard let account = account else { return } guard let account = account else { return }
webFeed.name = name webFeed.name = name
@ -192,12 +192,12 @@ private extension CloudKitAcountZoneDelegate {
} }
} }
func addExistingUnclaimedWebFeed(_ webFeed: WebFeed, containerExternalID: String) { func addExistingUnclaimedWebFeed(_ webFeed: Feed, containerExternalID: String) {
if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] { if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] {
unclaimedWebFeeds.append(webFeed) unclaimedWebFeeds.append(webFeed)
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
} else { } else {
var unclaimedWebFeeds = [WebFeed]() var unclaimedWebFeeds = [Feed]()
unclaimedWebFeeds.append(webFeed) unclaimedWebFeeds.append(webFeed)
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
} }

View File

@ -19,7 +19,7 @@ extension Notification.Name {
public protocol Container: AnyObject, ContainerIdentifiable { public protocol Container: AnyObject, ContainerIdentifiable {
var account: Account? { get } var account: Account? { get }
var topLevelWebFeeds: Set<WebFeed> { get set } var topLevelWebFeeds: Set<Feed> { get set }
var folders: Set<Folder>? { get set } var folders: Set<Folder>? { get set }
var externalID: String? { get set } var externalID: String? { get set }
@ -29,17 +29,17 @@ public protocol Container: AnyObject, ContainerIdentifiable {
func hasChildFolder(with: String) -> Bool func hasChildFolder(with: String) -> Bool
func childFolder(with: String) -> Folder? func childFolder(with: String) -> Folder?
func removeWebFeed(_ webFeed: WebFeed) func removeWebFeed(_ webFeed: Feed)
func addWebFeed(_ webFeed: WebFeed) func addWebFeed(_ webFeed: Feed)
//Recursive  checks subfolders //Recursive  checks subfolders
func flattenedWebFeeds() -> Set<WebFeed> func flattenedWebFeeds() -> Set<Feed>
func has(_ webFeed: WebFeed) -> Bool func has(_ webFeed: Feed) -> Bool
func hasWebFeed(with webFeedID: String) -> Bool func hasWebFeed(with webFeedID: String) -> Bool
func hasWebFeed(withURL url: String) -> Bool func hasWebFeed(withURL url: String) -> Bool
func existingWebFeed(withWebFeedID: String) -> WebFeed? func existingWebFeed(withWebFeedID: String) -> Feed?
func existingWebFeed(withURL url: String) -> WebFeed? func existingWebFeed(withURL url: String) -> Feed?
func existingWebFeed(withExternalID externalID: String) -> WebFeed? func existingWebFeed(withExternalID externalID: String) -> Feed?
func existingFolder(with name: String) -> Folder? func existingFolder(with name: String) -> Folder?
func existingFolder(withID: Int) -> Folder? func existingFolder(withID: Int) -> Folder?
@ -69,7 +69,7 @@ public extension Container {
} }
func objectIsChild(_ object: AnyObject) -> Bool { func objectIsChild(_ object: AnyObject) -> Bool {
if let feed = object as? WebFeed { if let feed = object as? Feed {
return topLevelWebFeeds.contains(feed) return topLevelWebFeeds.contains(feed)
} }
if let folder = object as? Folder { if let folder = object as? Folder {
@ -78,8 +78,8 @@ public extension Container {
return false return false
} }
func flattenedWebFeeds() -> Set<WebFeed> { func flattenedWebFeeds() -> Set<Feed> {
var feeds = Set<WebFeed>() var feeds = Set<Feed>()
feeds.formUnion(topLevelWebFeeds) feeds.formUnion(topLevelWebFeeds)
if let folders = folders { if let folders = folders {
for folder in folders { for folder in folders {
@ -97,11 +97,11 @@ public extension Container {
return existingWebFeed(withURL: url) != nil return existingWebFeed(withURL: url) != nil
} }
func has(_ webFeed: WebFeed) -> Bool { func has(_ webFeed: Feed) -> Bool {
return flattenedWebFeeds().contains(webFeed) return flattenedWebFeeds().contains(webFeed)
} }
func existingWebFeed(withWebFeedID webFeedID: String) -> WebFeed? { func existingWebFeed(withWebFeedID webFeedID: String) -> Feed? {
for feed in flattenedWebFeeds() { for feed in flattenedWebFeeds() {
if feed.webFeedID == webFeedID { if feed.webFeedID == webFeedID {
return feed return feed
@ -110,7 +110,7 @@ public extension Container {
return nil return nil
} }
func existingWebFeed(withURL url: String) -> WebFeed? { func existingWebFeed(withURL url: String) -> Feed? {
for feed in flattenedWebFeeds() { for feed in flattenedWebFeeds() {
if feed.url == url { if feed.url == url {
return feed return feed
@ -119,7 +119,7 @@ public extension Container {
return nil return nil
} }
func existingWebFeed(withExternalID externalID: String) -> WebFeed? { func existingWebFeed(withExternalID externalID: String) -> Feed? {
for feed in flattenedWebFeeds() { for feed in flattenedWebFeeds() {
if feed.externalID == externalID { if feed.externalID == externalID {
return feed return feed

View File

@ -14,7 +14,7 @@ public extension Notification.Name {
static let WebFeedSettingDidChange = Notification.Name(rawValue: "FeedSettingDidChangeNotification") static let WebFeedSettingDidChange = Notification.Name(rawValue: "FeedSettingDidChangeNotification")
} }
public extension WebFeed { public extension Feed {
static let WebFeedSettingUserInfoKey = "feedSetting" static let WebFeedSettingUserInfoKey = "feedSetting"
@ -30,7 +30,7 @@ public extension WebFeed {
} }
} }
extension WebFeed { extension Feed {
func takeSettings(from parsedFeed: ParsedFeed) { func takeSettings(from parsedFeed: ParsedFeed) {
iconURL = parsedFeed.iconURL iconURL = parsedFeed.iconURL
@ -41,7 +41,7 @@ extension WebFeed {
} }
func postFeedSettingDidChangeNotification(_ codingKey: WebFeedMetadata.CodingKeys) { func postFeedSettingDidChangeNotification(_ codingKey: WebFeedMetadata.CodingKeys) {
let userInfo = [WebFeed.WebFeedSettingUserInfoKey: codingKey.stringValue] let userInfo = [Feed.WebFeedSettingUserInfoKey: codingKey.stringValue]
NotificationCenter.default.post(name: .WebFeedSettingDidChange, object: self, userInfo: userInfo) NotificationCenter.default.post(name: .WebFeedSettingDidChange, object: self, userInfo: userInfo)
} }
} }
@ -56,7 +56,7 @@ public extension Article {
return manager.existingAccount(with: accountID) return manager.existingAccount(with: accountID)
} }
var webFeed: WebFeed? { var webFeed: Feed? {
return account?.existingWebFeed(withWebFeedID: webFeedID) return account?.existingWebFeed(withWebFeedID: webFeedID)
} }
} }

View File

@ -1,39 +1,322 @@
// //
// Feed.swift // WebFeed.swift
// Account // NetNewsWire
// //
// Created by Maurice Parker on 11/15/19. // Created by Brent Simmons on 7/1/17.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved. // Copyright © 2017 Ranchero Software, LLC. All rights reserved.
// //
import Foundation import Foundation
import RSCore import RSCore
import RSWeb
import Articles
public enum ReadFilterType { public final class Feed: SidebarItem, Renamable, Hashable {
case read
case none
case alwaysRead
}
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider { public var defaultReadFilterType: ReadFilterType {
return .none
var account: Account? { get }
var defaultReadFilterType: ReadFilterType { get }
}
public extension Feed {
func readFiltered(readFilterEnabledTable: [FeedIdentifier: Bool]) -> Bool {
guard defaultReadFilterType != .alwaysRead else {
return true
} }
if let feedID = feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
return readFilterEnabled public var sidebarItemID: SidebarItemIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return SidebarItemIdentifier.webFeed(accountID, webFeedID)
}
public weak var account: Account?
public let url: String
public var webFeedID: String {
get {
return metadata.webFeedID
}
set {
metadata.webFeedID = newValue
}
}
public var homePageURL: String? {
get {
return metadata.homePageURL
}
set {
if let url = newValue, !url.isEmpty {
metadata.homePageURL = url.normalizedURL
}
else {
metadata.homePageURL = nil
}
}
}
// Note: this is available only if the icon URL was available in the feed.
// The icon URL is a JSON-Feed-only feature.
// Otherwise we find an icon URL via other means, but we dont store it
// as part of feed metadata.
public var iconURL: String? {
get {
return metadata.iconURL
}
set {
metadata.iconURL = newValue
}
}
// Note: this is available only if the favicon URL was available in the feed.
// The favicon URL is a JSON-Feed-only feature.
// Otherwise we find a favicon URL via other means, but we dont store it
// as part of feed metadata.
public var faviconURL: String? {
get {
return metadata.faviconURL
}
set {
metadata.faviconURL = newValue
}
}
public var name: String? {
didSet {
if name != oldValue {
postDisplayNameDidChangeNotification()
}
}
}
public var authors: Set<Author>? {
get {
if let authorsArray = metadata.authors {
return Set(authorsArray)
}
return nil
}
set {
if let authorsSet = newValue {
metadata.authors = Array(authorsSet)
}
else {
metadata.authors = nil
}
}
}
public var editedName: String? {
// Dont let editedName == ""
get {
guard let s = metadata.editedName, !s.isEmpty else {
return nil
}
return s
}
set {
if newValue != editedName {
if let valueToSet = newValue, !valueToSet.isEmpty {
metadata.editedName = valueToSet
}
else {
metadata.editedName = nil
}
postDisplayNameDidChangeNotification()
}
}
}
public var conditionalGetInfo: HTTPConditionalGetInfo? {
get {
return metadata.conditionalGetInfo
}
set {
metadata.conditionalGetInfo = newValue
}
}
public var contentHash: String? {
get {
return metadata.contentHash
}
set {
metadata.contentHash = newValue
}
}
public var isNotifyAboutNewArticles: Bool? {
get {
return metadata.isNotifyAboutNewArticles
}
set {
metadata.isNotifyAboutNewArticles = newValue
}
}
public var isArticleExtractorAlwaysOn: Bool? {
get {
metadata.isArticleExtractorAlwaysOn
}
set {
metadata.isArticleExtractorAlwaysOn = newValue
}
}
public var sinceToken: String? {
get {
return metadata.sinceToken
}
set {
metadata.sinceToken = newValue
}
}
public var externalID: String? {
get {
return metadata.externalID
}
set {
metadata.externalID = newValue
}
}
// Folder Name: Sync Service Relationship ID
public var folderRelationship: [String: String]? {
get {
return metadata.folderRelationship
}
set {
metadata.folderRelationship = newValue
}
}
// MARK: - DisplayNameProvider
public var nameForDisplay: String {
if let s = editedName, !s.isEmpty {
return s
}
if let s = name, !s.isEmpty {
return s
}
return NSLocalizedString("Untitled", comment: "Feed name")
}
// MARK: - Renamable
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard let account = account else { return }
account.renameWebFeed(self, to: newName, completion: completion)
}
// MARK: - UnreadCountProvider
public var unreadCount: Int {
get {
return account?.unreadCount(for: self) ?? 0
}
set {
if unreadCount == newValue {
return
}
account?.setUnreadCount(newValue, for: self)
postUnreadCountDidChangeNotification()
}
}
// MARK: - NotificationDisplayName
public var notificationDisplayName: String {
#if os(macOS)
if self.url.contains("www.reddit.com") {
return NSLocalizedString("Show notifications for new posts", comment: "notifyNameDisplay / Reddit")
} else { } else {
return defaultReadFilterType == .read return NSLocalizedString("Show notifications for new articles", comment: "notifyNameDisplay / Default")
}
#else
if self.url.contains("www.reddit.com") {
return NSLocalizedString("Notify about new posts", comment: "notifyNameDisplay / Reddit")
} else {
return NSLocalizedString("Notify about new articles", comment: "notifyNameDisplay / Default")
}
#endif
} }
var metadata: WebFeedMetadata
// MARK: - Private
private let accountID: String // Used for hashing and equality; account may turn nil
// MARK: - Init
init(account: Account, url: String, metadata: WebFeedMetadata) {
self.account = account
self.accountID = account.accountID
self.url = url
self.metadata = metadata
}
// MARK: - API
public func dropConditionalGetInfo() {
conditionalGetInfo = nil
contentHash = nil
sinceToken = nil
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(webFeedID)
}
// MARK: - Equatable
public class func ==(lhs: Feed, rhs: Feed) -> Bool {
return lhs.webFeedID == rhs.webFeedID && lhs.accountID == rhs.accountID
}
}
// MARK: - OPMLRepresentable
extension Feed: OPMLRepresentable {
public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
// https://github.com/brentsimmons/NetNewsWire/issues/527
// Dont use nameForDisplay because that can result in a feed name "Untitled" written to disk,
// which NetNewsWire may take later to be the actual name.
var nameToUse = editedName
if nameToUse == nil {
nameToUse = name
}
if nameToUse == nil {
nameToUse = ""
}
let escapedName = nameToUse!.escapingSpecialXMLCharacters
var escapedHomePageURL = ""
if let homePageURL = homePageURL {
escapedHomePageURL = homePageURL.escapingSpecialXMLCharacters
}
let escapedFeedURL = url.escapingSpecialXMLCharacters
var s = "<outline text=\"\(escapedName)\" title=\"\(escapedName)\" description=\"\" type=\"rss\" version=\"RSS\" htmlUrl=\"\(escapedHomePageURL)\" xmlUrl=\"\(escapedFeedURL)\"/>\n"
s = s.prepending(tabCount: indentLevel)
return s
}
}
extension Set where Element == Feed {
func webFeedIDs() -> Set<String> {
return Set<String>(map { $0.webFeedID })
}
func sorted() -> Array<Feed> {
return sorted(by: { (webFeed1, webFeed2) -> Bool in
if webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedSame {
return webFeed1.url < webFeed2.url
}
return webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedAscending
})
} }
} }

View File

@ -169,7 +169,7 @@ final class FeedWranglerAPICaller: NSObject {
} }
func retrieveFeedItems(page: Int = 0, feed: WebFeed? = nil, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) { func retrieveFeedItems(page: Int = 0, feed: Feed? = nil, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
let queryItems = [ let queryItems = [
URLQueryItem(name: "read", value: "false"), URLQueryItem(name: "read", value: "false"),
URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)), URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)),

View File

@ -315,7 +315,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
fatalError() fatalError()
} }
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(2) refreshProgress.addToNumberOfTasksAndRemaining(2)
self.refreshCredentials(for: account) { self.refreshCredentials(for: account) {
@ -336,7 +336,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
} }
} }
private func addFeedWranglerSubscription(account: Account, subscription sub: FeedWranglerSubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) { private func addFeedWranglerSubscription(account: Account, subscription sub: FeedWranglerSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
DispatchQueue.main.async { DispatchQueue.main.async {
let feed = account.createWebFeed(with: sub.title, url: sub.feedURL, webFeedID: String(sub.feedID), homePageURL: sub.siteURL) let feed = account.createWebFeed(with: sub.title, url: sub.feedURL, webFeedID: String(sub.feedID), homePageURL: sub.siteURL)
@ -364,7 +364,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
} }
} }
private func initialFeedDownload(account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) { private func initialFeedDownload(account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
self.caller.retrieveFeedItems(page: 0, feed: feed) { results in self.caller.retrieveFeedItems(page: 0, feed: feed) { results in
switch results { switch results {
@ -383,7 +383,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
} }
} }
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) { func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(2) refreshProgress.addToNumberOfTasksAndRemaining(2)
self.refreshCredentials(for: account) { self.refreshCredentials(for: account) {
@ -408,7 +408,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
} }
} }
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
// just add to account, folders are not supported // just add to account, folders are not supported
DispatchQueue.main.async { DispatchQueue.main.async {
account.addFeedIfNotInAnyFolder(feed) account.addFeedIfNotInAnyFolder(feed)
@ -416,7 +416,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
} }
} }
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(2) refreshProgress.addToNumberOfTasksAndRemaining(2)
self.refreshCredentials(for: account) { self.refreshCredentials(for: account) {
@ -442,11 +442,11 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
} }
} }
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
fatalError() fatalError()
} }
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let existingFeed = account.existingWebFeed(withURL: feed.url) { if let existingFeed = account.existingWebFeed(withURL: feed.url) {
account.addWebFeed(existingFeed, to: container) { result in account.addWebFeed(existingFeed, to: container) { result in
switch result { switch result {

View File

@ -388,7 +388,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
} }
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
caller.createSubscription(url: url) { result in caller.createSubscription(url: url) { result in
@ -420,7 +420,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
} }
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) { func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
// This error should never happen // This error should never happen
guard let subscriptionID = feed.externalID else { guard let subscriptionID = feed.externalID else {
@ -447,7 +447,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
} }
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if feed.folderRelationship?.count ?? 0 > 1 { if feed.folderRelationship?.count ?? 0 > 1 {
deleteTagging(for: account, with: feed, from: container, completion: completion) deleteTagging(for: account, with: feed, from: container, completion: completion)
} else { } else {
@ -455,7 +455,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
} }
} }
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if from is Account { if from is Account {
addWebFeed(for: account, with: feed, to: to, completion: completion) addWebFeed(for: account, with: feed, to: to, completion: completion)
} else { } else {
@ -470,7 +470,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
} }
} }
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let folder = container as? Folder, let webFeedID = Int(feed.webFeedID) { if let folder = container as? Folder, let webFeedID = Int(feed.webFeedID) {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
@ -502,7 +502,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
} }
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let existingFeed = account.existingWebFeed(withURL: feed.url) { if let existingFeed = account.existingWebFeed(withURL: feed.url) {
account.addWebFeed(existingFeed, to: container) { result in account.addWebFeed(existingFeed, to: container) { result in
@ -990,14 +990,14 @@ private extension FeedbinAccountDelegate {
} }
} }
func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) { func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
if var folderRelationship = feed.folderRelationship { if var folderRelationship = feed.folderRelationship {
folderRelationship[folderName] = nil folderRelationship[folderName] = nil
feed.folderRelationship = folderRelationship feed.folderRelationship = folderRelationship
} }
} }
func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) { func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) {
if var folderRelationship = feed.folderRelationship { if var folderRelationship = feed.folderRelationship {
folderRelationship[folderName] = id folderRelationship[folderName] = id
feed.folderRelationship = folderRelationship feed.folderRelationship = folderRelationship
@ -1006,7 +1006,7 @@ private extension FeedbinAccountDelegate {
} }
} }
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<WebFeed, Error>) -> Void) { func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<Feed, Error>) -> Void) {
var orderFound = 0 var orderFound = 0
let feedSpecifiers: [FeedSpecifier] = choices.map { choice in let feedSpecifiers: [FeedSpecifier] = choices.map { choice in
@ -1025,7 +1025,7 @@ private extension FeedbinAccountDelegate {
} }
} }
func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -1058,7 +1058,7 @@ private extension FeedbinAccountDelegate {
} }
func initialFeedDownload( account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) { func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
// refreshArticles is being reused and will clear one of the tasks for us // refreshArticles is being reused and will clear one of the tasks for us
refreshProgress.addToNumberOfTasksAndRemaining(4) refreshProgress.addToNumberOfTasksAndRemaining(4)
@ -1371,7 +1371,7 @@ private extension FeedbinAccountDelegate {
} }
func deleteTagging(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) { func deleteTagging(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] { if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
@ -1401,7 +1401,7 @@ private extension FeedbinAccountDelegate {
} }
func deleteSubscription(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) { func deleteSubscription(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
// This error should never happen // This error should never happen
guard let subscriptionID = feed.externalID else { guard let subscriptionID = feed.externalID else {

View File

@ -314,7 +314,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
} }
} }
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
do { do {
guard let credentials = credentials else { guard let credentials = credentials else {
@ -347,7 +347,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
} }
} }
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) { func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID } let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID }
guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else { guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else {
completion(.failure(FeedlyAccountDelegateError.unableToRenameFeed(feed.nameForDisplay, name))) completion(.failure(FeedlyAccountDelegateError.unableToRenameFeed(feed.nameForDisplay, name)))
@ -374,7 +374,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
feed.editedName = name feed.editedName = name
} }
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
do { do {
guard let credentials = credentials else { guard let credentials = credentials else {
@ -405,7 +405,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
} }
} }
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let folder = container as? Folder, let collectionId = folder.externalID else { guard let folder = container as? Folder, let collectionId = folder.externalID else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed))) completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
@ -425,7 +425,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
folder.removeWebFeed(feed) folder.removeWebFeed(feed)
} }
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let from = from as? Folder, let to = to as? Folder else { guard let from = from as? Folder, let to = to as? Folder else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder)) completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
@ -458,7 +458,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
to.addWebFeed(feed) to.addWebFeed(feed)
} }
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let existingFeed = account.existingWebFeed(withURL: feed.url) { if let existingFeed = account.existingWebFeed(withURL: feed.url) {
account.addWebFeed(existingFeed, to: container) { result in account.addWebFeed(existingFeed, to: container) { result in
switch result { switch result {

View File

@ -14,11 +14,11 @@ enum FeedlyAccountDelegateError: LocalizedError {
case unableToAddFolder(String) case unableToAddFolder(String)
case unableToRenameFolder(String, String) case unableToRenameFolder(String, String)
case unableToRemoveFolder(String) case unableToRemoveFolder(String)
case unableToMoveFeedBetweenFolders(WebFeed, Folder, Folder) case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
case addFeedChooseFolder case addFeedChooseFolder
case addFeedInvalidFolder(Folder) case addFeedInvalidFolder(Folder)
case unableToRenameFeed(String, String) case unableToRenameFeed(String, String)
case unableToRemoveFeed(WebFeed) case unableToRemoveFeed(Feed)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {

View File

@ -28,7 +28,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
private let getStreamContentsService: FeedlyGetStreamContentsService private let getStreamContentsService: FeedlyGetStreamContentsService
private let log: OSLog private let log: OSLog
private var feedResourceId: FeedlyFeedResourceId? private var feedResourceId: FeedlyFeedResourceId?
var addCompletionHandler: ((Result<WebFeed, Error>) -> ())? var addCompletionHandler: ((Result<Feed, Error>) -> ())?
init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws { init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws {

View File

@ -46,7 +46,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
} }
// Pair each Feed with its Folder. // Pair each Feed with its Folder.
var feedsAdded = Set<WebFeed>() var feedsAdded = Set<Feed>()
let feedsAndFolders = pairs let feedsAndFolders = pairs
.map({ (collectionFeeds, folder) -> [(FeedlyFeed, Folder)] in .map({ (collectionFeeds, folder) -> [(FeedlyFeed, Folder)] in
@ -55,7 +55,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
} }
}) })
.flatMap { $0 } .flatMap { $0 }
.compactMap { (collectionFeed, folder) -> (WebFeed, Folder) in .compactMap { (collectionFeed, folder) -> (Feed, Folder) in
// find an existing feed previously added to the account // find an existing feed previously added to the account
if let feed = account.existingWebFeed(withWebFeedID: collectionFeed.id) { if let feed = account.existingWebFeed(withWebFeedID: collectionFeed.id) {

View File

@ -10,7 +10,7 @@ import Foundation
import Articles import Articles
import RSCore import RSCore
public final class Folder: Feed, Renamable, Container, Hashable { public final class Folder: SidebarItem, Renamable, Container, Hashable {
public var defaultReadFilterType: ReadFilterType { public var defaultReadFilterType: ReadFilterType {
return .read return .read
@ -24,16 +24,16 @@ public final class Folder: Feed, Renamable, Container, Hashable {
return ContainerIdentifier.folder(accountID, nameForDisplay) return ContainerIdentifier.folder(accountID, nameForDisplay)
} }
public var feedID: FeedIdentifier? { public var sidebarItemID: SidebarItemIdentifier? {
guard let accountID = account?.accountID else { guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.") assertionFailure("Expected feed.account, but got nil.")
return nil return nil
} }
return FeedIdentifier.folder(accountID, nameForDisplay) return SidebarItemIdentifier.folder(accountID, nameForDisplay)
} }
public weak var account: Account? public weak var account: Account?
public var topLevelWebFeeds: Set<WebFeed> = Set<WebFeed>() public var topLevelWebFeeds: Set<Feed> = Set<Feed>()
public var folders: Set<Folder>? = nil // subfolders are not supported, so this is always nil public var folders: Set<Folder>? = nil // subfolders are not supported, so this is always nil
public var name: String? { public var name: String? {
@ -100,25 +100,25 @@ public final class Folder: Feed, Renamable, Container, Hashable {
// MARK: Container // MARK: Container
public func flattenedWebFeeds() -> Set<WebFeed> { public func flattenedWebFeeds() -> Set<Feed> {
// Since sub-folders are not supported, its always the top-level feeds. // Since sub-folders are not supported, its always the top-level feeds.
return topLevelWebFeeds return topLevelWebFeeds
} }
public func objectIsChild(_ object: AnyObject) -> Bool { public func objectIsChild(_ object: AnyObject) -> Bool {
// Folders contain Feed objects only, at least for now. // Folders contain Feed objects only, at least for now.
guard let feed = object as? WebFeed else { guard let feed = object as? Feed else {
return false return false
} }
return topLevelWebFeeds.contains(feed) return topLevelWebFeeds.contains(feed)
} }
public func addWebFeed(_ feed: WebFeed) { public func addWebFeed(_ feed: Feed) {
topLevelWebFeeds.insert(feed) topLevelWebFeeds.insert(feed)
postChildrenDidChangeNotification() postChildrenDidChangeNotification()
} }
public func addFeeds(_ feeds: Set<WebFeed>) { public func addFeeds(_ feeds: Set<Feed>) {
guard !feeds.isEmpty else { guard !feeds.isEmpty else {
return return
} }
@ -126,12 +126,12 @@ public final class Folder: Feed, Renamable, Container, Hashable {
postChildrenDidChangeNotification() postChildrenDidChangeNotification()
} }
public func removeWebFeed(_ feed: WebFeed) { public func removeWebFeed(_ feed: Feed) {
topLevelWebFeeds.remove(feed) topLevelWebFeeds.remove(feed)
postChildrenDidChangeNotification() postChildrenDidChangeNotification()
} }
public func removeFeeds(_ feeds: Set<WebFeed>) { public func removeFeeds(_ feeds: Set<Feed>) {
guard !feeds.isEmpty else { guard !feeds.isEmpty else {
return return
} }
@ -164,7 +164,7 @@ private extension Folder {
unreadCount = updatedUnreadCount unreadCount = updatedUnreadCount
} }
func childrenContain(_ feed: WebFeed) -> Bool { func childrenContain(_ feed: Feed) -> Bool {
return topLevelWebFeeds.contains(feed) return topLevelWebFeeds.contains(feed)
} }
} }

View File

@ -122,7 +122,7 @@ final class LocalAccountDelegate: AccountDelegate {
} }
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
completion(.failure(LocalAccountDelegateError.invalidParameter)) completion(.failure(LocalAccountDelegateError.invalidParameter))
return return
@ -131,28 +131,28 @@ final class LocalAccountDelegate: AccountDelegate {
createRSSWebFeed(for: account, url: url, editedName: name, container: container, completion: completion) createRSSWebFeed(for: account, url: url, editedName: name, container: container, completion: completion)
} }
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) { func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
feed.editedName = name feed.editedName = name
completion(.success(())) completion(.success(()))
} }
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
container.removeWebFeed(feed) container.removeWebFeed(feed)
completion(.success(())) completion(.success(()))
} }
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
from.removeWebFeed(feed) from.removeWebFeed(feed)
to.addWebFeed(feed) to.addWebFeed(feed)
completion(.success(())) completion(.success(()))
} }
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
container.addWebFeed(feed) container.addWebFeed(feed)
completion(.success(())) completion(.success(()))
} }
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
container.addWebFeed(feed) container.addWebFeed(feed)
completion(.success(())) completion(.success(()))
} }
@ -219,7 +219,7 @@ final class LocalAccountDelegate: AccountDelegate {
extension LocalAccountDelegate: LocalAccountRefresherDelegate { extension LocalAccountDelegate: LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) { func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed) {
refreshProgress.completeTask() refreshProgress.completeTask()
} }
@ -231,7 +231,7 @@ extension LocalAccountDelegate: LocalAccountRefresherDelegate {
private extension LocalAccountDelegate { private extension LocalAccountDelegate {
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
// We need to use a batch update here because we need to assign add the feed to the // We need to use a batch update here because we need to assign add the feed to the
// container before the name has been downloaded. This will put it in the sidebar // container before the name has been downloaded. This will put it in the sidebar

View File

@ -14,7 +14,7 @@ import Articles
import ArticlesDatabase import ArticlesDatabase
protocol LocalAccountRefresherDelegate { protocol LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed)
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void) func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void)
} }
@ -28,7 +28,7 @@ final class LocalAccountRefresher {
return DownloadSession(delegate: self) return DownloadSession(delegate: self)
}() }()
public func refreshFeeds(_ feeds: Set<WebFeed>, completion: (() -> Void)? = nil) { public func refreshFeeds(_ feeds: Set<Feed>, completion: (() -> Void)? = nil) {
guard !feeds.isEmpty else { guard !feeds.isEmpty else {
completion?() completion?()
return return
@ -53,7 +53,7 @@ final class LocalAccountRefresher {
extension LocalAccountRefresher: DownloadSessionDelegate { extension LocalAccountRefresher: DownloadSessionDelegate {
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? { func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
guard let feed = representedObject as? WebFeed else { guard let feed = representedObject as? Feed else {
return nil return nil
} }
guard let url = URL(string: feed.url) else { guard let url = URL(string: feed.url) else {
@ -69,7 +69,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
} }
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) { func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
let feed = representedObject as! WebFeed let feed = representedObject as! Feed
guard !data.isEmpty, !isSuspended else { guard !data.isEmpty, !isSuspended else {
completion() completion()
@ -120,7 +120,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
} }
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool { func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
let feed = representedObject as! WebFeed let feed = representedObject as! Feed
guard !isSuspended else { guard !isSuspended else {
delegate?.localAccountRefresher(self, requestCompletedFor: feed) delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return false return false
@ -139,17 +139,17 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
} }
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) { func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
let feed = representedObject as! WebFeed let feed = representedObject as! Feed
delegate?.localAccountRefresher(self, requestCompletedFor: feed) delegate?.localAccountRefresher(self, requestCompletedFor: feed)
} }
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) { func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
let feed = representedObject as! WebFeed let feed = representedObject as! Feed
delegate?.localAccountRefresher(self, requestCompletedFor: feed) delegate?.localAccountRefresher(self, requestCompletedFor: feed)
} }
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) { func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
let feed = representedObject as! WebFeed let feed = representedObject as! Feed
delegate?.localAccountRefresher(self, requestCompletedFor: feed) delegate?.localAccountRefresher(self, requestCompletedFor: feed)
} }

View File

@ -195,14 +195,14 @@ extension NewsBlurAccountDelegate {
} }
func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) { func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
if var folderRelationship = feed.folderRelationship { if var folderRelationship = feed.folderRelationship {
folderRelationship[folderName] = nil folderRelationship[folderName] = nil
feed.folderRelationship = folderRelationship feed.folderRelationship = folderRelationship
} }
} }
func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) { func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) {
if var folderRelationship = feed.folderRelationship { if var folderRelationship = feed.folderRelationship {
folderRelationship[folderName] = id folderRelationship[folderName] = id
feed.folderRelationship = folderRelationship feed.folderRelationship = folderRelationship
@ -412,7 +412,7 @@ extension NewsBlurAccountDelegate {
} }
} }
func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
guard let feed = feed else { guard let feed = feed else {
completion(.failure(NewsBlurError.invalidParameter)) completion(.failure(NewsBlurError.invalidParameter))
return return
@ -445,7 +445,7 @@ extension NewsBlurAccountDelegate {
} }
} }
func downloadFeed(account: Account, feed: WebFeed, page: Int, completion: @escaping (Result<Void, Error>) -> Void) { func downloadFeed(account: Account, feed: Feed, page: Int, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
caller.retrieveStories(feedID: feed.webFeedID, page: page) { result in caller.retrieveStories(feedID: feed.webFeedID, page: page) { result in
@ -484,7 +484,7 @@ extension NewsBlurAccountDelegate {
} }
} }
func initialFeedDownload(account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) { func initialFeedDownload(account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
// Download the initial articles // Download the initial articles
@ -513,7 +513,7 @@ extension NewsBlurAccountDelegate {
} }
} }
func deleteFeed(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) { func deleteFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
// This error should never happen // This error should never happen
guard let feedID = feed.externalID else { guard let feedID = feed.externalID else {
completion(.failure(NewsBlurError.invalidParameter)) completion(.failure(NewsBlurError.invalidParameter))

View File

@ -423,7 +423,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
} }
} }
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> ()) { func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> ()) {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
let folderName = (container as? Folder)?.name let folderName = (container as? Folder)?.name
@ -442,7 +442,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
} }
} }
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) { func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
guard let feedID = feed.externalID else { guard let feedID = feed.externalID else {
completion(.failure(NewsBlurError.invalidParameter)) completion(.failure(NewsBlurError.invalidParameter))
return return
@ -469,7 +469,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
} }
} }
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) { func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
guard let folder = container as? Folder else { guard let folder = container as? Folder else {
DispatchQueue.main.async { DispatchQueue.main.async {
if let account = container as? Account { if let account = container as? Account {
@ -488,11 +488,11 @@ final class NewsBlurAccountDelegate: AccountDelegate {
completion(.success(())) completion(.success(()))
} }
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) { func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
deleteFeed(for: account, with: feed, from: container, completion: completion) deleteFeed(for: account, with: feed, from: container, completion: completion)
} }
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) { func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
guard let feedID = feed.externalID else { guard let feedID = feed.externalID else {
completion(.failure(NewsBlurError.invalidParameter)) completion(.failure(NewsBlurError.invalidParameter))
return return
@ -519,7 +519,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
} }
} }
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) { func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
if let existingFeed = account.existingWebFeed(withURL: feed.url) { if let existingFeed = account.existingWebFeed(withURL: feed.url) {
account.addWebFeed(existingFeed, to: container) { result in account.addWebFeed(existingFeed, to: container) { result in
switch result { switch result {
@ -547,7 +547,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
return return
} }
var feedsToRestore: [WebFeed] = [] var feedsToRestore: [Feed] = []
for feed in folder.topLevelWebFeeds { for feed in folder.topLevelWebFeeds {
feedsToRestore.append(feed) feedsToRestore.append(feed)
folder.topLevelWebFeeds.remove(feed) folder.topLevelWebFeeds.remove(feed)

View File

@ -390,7 +390,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
guard let url = URL(string: url) else { guard let url = URL(string: url) else {
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
return return
@ -439,7 +439,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) { func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
// This error should never happen // This error should never happen
guard let subscriptionID = feed.externalID else { guard let subscriptionID = feed.externalID else {
@ -466,7 +466,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let subscriptionID = feed.externalID else { guard let subscriptionID = feed.externalID else {
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
return return
@ -496,7 +496,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
} }
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) { func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if from is Account { if from is Account {
addWebFeed(for: account, with: feed, to: to, completion: completion) addWebFeed(for: account, with: feed, to: to, completion: completion)
} else { } else {
@ -524,7 +524,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
} }
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let folder = container as? Folder, let feedExternalID = feed.externalID { if let folder = container as? Folder, let feedExternalID = feed.externalID {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
caller.createTagging(subscriptionID: feedExternalID, tagName: folder.name ?? "") { result in caller.createTagging(subscriptionID: feedExternalID, tagName: folder.name ?? "") { result in
@ -554,7 +554,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
} }
} }
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) { func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let existingFeed = account.existingWebFeed(withURL: feed.url) { if let existingFeed = account.existingWebFeed(withURL: feed.url) {
account.addWebFeed(existingFeed, to: container) { result in account.addWebFeed(existingFeed, to: container) { result in
@ -901,13 +901,13 @@ private extension ReaderAPIAccountDelegate {
} }
func clearFolderRelationship(for feed: WebFeed, folderExternalID: String?) { func clearFolderRelationship(for feed: Feed, folderExternalID: String?) {
guard var folderRelationship = feed.folderRelationship, let folderExternalID = folderExternalID else { return } guard var folderRelationship = feed.folderRelationship, let folderExternalID = folderExternalID else { return }
folderRelationship[folderExternalID] = nil folderRelationship[folderExternalID] = nil
feed.folderRelationship = folderRelationship feed.folderRelationship = folderRelationship
} }
func saveFolderRelationship(for feed: WebFeed, folderExternalID: String?, feedExternalID: String) { func saveFolderRelationship(for feed: Feed, folderExternalID: String?, feedExternalID: String) {
guard let folderExternalID = folderExternalID else { return } guard let folderExternalID = folderExternalID else { return }
if var folderRelationship = feed.folderRelationship { if var folderRelationship = feed.folderRelationship {
folderRelationship[folderExternalID] = feedExternalID folderRelationship[folderExternalID] = feedExternalID
@ -917,7 +917,7 @@ private extension ReaderAPIAccountDelegate {
} }
} }
func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -948,7 +948,7 @@ private extension ReaderAPIAccountDelegate {
} }
func initialFeedDownload( account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) { func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(5) refreshProgress.addToNumberOfTasksAndRemaining(5)
// Download the initial articles // Download the initial articles

View File

@ -0,0 +1,39 @@
//
// SidebarItem.swift
// Account
//
// Created by Maurice Parker on 11/15/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
public enum ReadFilterType {
case read
case none
case alwaysRead
}
public protocol SidebarItem: SidebarItemIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
var account: Account? { get }
var defaultReadFilterType: ReadFilterType { get }
}
public extension SidebarItem {
func readFiltered(readFilterEnabledTable: [SidebarItemIdentifier: Bool]) -> Bool {
guard defaultReadFilterType != .alwaysRead else {
return true
}
if let feedID = sidebarItemID, let readFilterEnabled = readFilterEnabledTable[feedID] {
return readFilterEnabled
} else {
return defaultReadFilterType == .read
}
}
}

View File

@ -8,11 +8,11 @@
import Foundation import Foundation
public protocol FeedIdentifiable { public protocol SidebarItemIdentifiable {
var feedID: FeedIdentifier? { get } var sidebarItemID: SidebarItemIdentifier? { get }
} }
public enum FeedIdentifier: CustomStringConvertible, Hashable, Equatable { public enum SidebarItemIdentifier: CustomStringConvertible, Hashable, Equatable {
case smartFeed(String) // String is a unique identifier case smartFeed(String) // String is a unique identifier
case script(String) // String is a unique identifier case script(String) // String is a unique identifier
@ -65,16 +65,16 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable, Equatable {
switch type { switch type {
case "smartFeed": case "smartFeed":
guard let id = userInfo["id"] as? String else { return nil } guard let id = userInfo["id"] as? String else { return nil }
self = FeedIdentifier.smartFeed(id) self = SidebarItemIdentifier.smartFeed(id)
case "script": case "script":
guard let id = userInfo["id"] as? String else { return nil } guard let id = userInfo["id"] as? String else { return nil }
self = FeedIdentifier.script(id) self = SidebarItemIdentifier.script(id)
case "feed": case "feed":
guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil } guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil }
self = FeedIdentifier.webFeed(accountID, webFeedID) self = SidebarItemIdentifier.webFeed(accountID, webFeedID)
case "folder": case "folder":
guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil } guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil }
self = FeedIdentifier.folder(accountID, folderName) self = SidebarItemIdentifier.folder(accountID, folderName)
default: default:
return nil return nil
} }

View File

@ -1,322 +0,0 @@
//
// WebFeed.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/1/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSWeb
import Articles
public final class WebFeed: Feed, Renamable, Hashable {
public var defaultReadFilterType: ReadFilterType {
return .none
}
public var feedID: FeedIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return FeedIdentifier.webFeed(accountID, webFeedID)
}
public weak var account: Account?
public let url: String
public var webFeedID: String {
get {
return metadata.webFeedID
}
set {
metadata.webFeedID = newValue
}
}
public var homePageURL: String? {
get {
return metadata.homePageURL
}
set {
if let url = newValue, !url.isEmpty {
metadata.homePageURL = url.normalizedURL
}
else {
metadata.homePageURL = nil
}
}
}
// Note: this is available only if the icon URL was available in the feed.
// The icon URL is a JSON-Feed-only feature.
// Otherwise we find an icon URL via other means, but we dont store it
// as part of feed metadata.
public var iconURL: String? {
get {
return metadata.iconURL
}
set {
metadata.iconURL = newValue
}
}
// Note: this is available only if the favicon URL was available in the feed.
// The favicon URL is a JSON-Feed-only feature.
// Otherwise we find a favicon URL via other means, but we dont store it
// as part of feed metadata.
public var faviconURL: String? {
get {
return metadata.faviconURL
}
set {
metadata.faviconURL = newValue
}
}
public var name: String? {
didSet {
if name != oldValue {
postDisplayNameDidChangeNotification()
}
}
}
public var authors: Set<Author>? {
get {
if let authorsArray = metadata.authors {
return Set(authorsArray)
}
return nil
}
set {
if let authorsSet = newValue {
metadata.authors = Array(authorsSet)
}
else {
metadata.authors = nil
}
}
}
public var editedName: String? {
// Dont let editedName == ""
get {
guard let s = metadata.editedName, !s.isEmpty else {
return nil
}
return s
}
set {
if newValue != editedName {
if let valueToSet = newValue, !valueToSet.isEmpty {
metadata.editedName = valueToSet
}
else {
metadata.editedName = nil
}
postDisplayNameDidChangeNotification()
}
}
}
public var conditionalGetInfo: HTTPConditionalGetInfo? {
get {
return metadata.conditionalGetInfo
}
set {
metadata.conditionalGetInfo = newValue
}
}
public var contentHash: String? {
get {
return metadata.contentHash
}
set {
metadata.contentHash = newValue
}
}
public var isNotifyAboutNewArticles: Bool? {
get {
return metadata.isNotifyAboutNewArticles
}
set {
metadata.isNotifyAboutNewArticles = newValue
}
}
public var isArticleExtractorAlwaysOn: Bool? {
get {
metadata.isArticleExtractorAlwaysOn
}
set {
metadata.isArticleExtractorAlwaysOn = newValue
}
}
public var sinceToken: String? {
get {
return metadata.sinceToken
}
set {
metadata.sinceToken = newValue
}
}
public var externalID: String? {
get {
return metadata.externalID
}
set {
metadata.externalID = newValue
}
}
// Folder Name: Sync Service Relationship ID
public var folderRelationship: [String: String]? {
get {
return metadata.folderRelationship
}
set {
metadata.folderRelationship = newValue
}
}
// MARK: - DisplayNameProvider
public var nameForDisplay: String {
if let s = editedName, !s.isEmpty {
return s
}
if let s = name, !s.isEmpty {
return s
}
return NSLocalizedString("Untitled", comment: "Feed name")
}
// MARK: - Renamable
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard let account = account else { return }
account.renameWebFeed(self, to: newName, completion: completion)
}
// MARK: - UnreadCountProvider
public var unreadCount: Int {
get {
return account?.unreadCount(for: self) ?? 0
}
set {
if unreadCount == newValue {
return
}
account?.setUnreadCount(newValue, for: self)
postUnreadCountDidChangeNotification()
}
}
// MARK: - NotificationDisplayName
public var notificationDisplayName: String {
#if os(macOS)
if self.url.contains("www.reddit.com") {
return NSLocalizedString("Show notifications for new posts", comment: "notifyNameDisplay / Reddit")
} else {
return NSLocalizedString("Show notifications for new articles", comment: "notifyNameDisplay / Default")
}
#else
if self.url.contains("www.reddit.com") {
return NSLocalizedString("Notify about new posts", comment: "notifyNameDisplay / Reddit")
} else {
return NSLocalizedString("Notify about new articles", comment: "notifyNameDisplay / Default")
}
#endif
}
var metadata: WebFeedMetadata
// MARK: - Private
private let accountID: String // Used for hashing and equality; account may turn nil
// MARK: - Init
init(account: Account, url: String, metadata: WebFeedMetadata) {
self.account = account
self.accountID = account.accountID
self.url = url
self.metadata = metadata
}
// MARK: - API
public func dropConditionalGetInfo() {
conditionalGetInfo = nil
contentHash = nil
sinceToken = nil
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(webFeedID)
}
// MARK: - Equatable
public class func ==(lhs: WebFeed, rhs: WebFeed) -> Bool {
return lhs.webFeedID == rhs.webFeedID && lhs.accountID == rhs.accountID
}
}
// MARK: - OPMLRepresentable
extension WebFeed: OPMLRepresentable {
public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
// https://github.com/brentsimmons/NetNewsWire/issues/527
// Dont use nameForDisplay because that can result in a feed name "Untitled" written to disk,
// which NetNewsWire may take later to be the actual name.
var nameToUse = editedName
if nameToUse == nil {
nameToUse = name
}
if nameToUse == nil {
nameToUse = ""
}
let escapedName = nameToUse!.escapingSpecialXMLCharacters
var escapedHomePageURL = ""
if let homePageURL = homePageURL {
escapedHomePageURL = homePageURL.escapingSpecialXMLCharacters
}
let escapedFeedURL = url.escapingSpecialXMLCharacters
var s = "<outline text=\"\(escapedName)\" title=\"\(escapedName)\" description=\"\" type=\"rss\" version=\"RSS\" htmlUrl=\"\(escapedHomePageURL)\" xmlUrl=\"\(escapedFeedURL)\"/>\n"
s = s.prepending(tabCount: indentLevel)
return s
}
}
extension Set where Element == WebFeed {
func webFeedIDs() -> Set<String> {
return Set<String>(map { $0.webFeedID })
}
func sorted() -> Array<WebFeed> {
return sorted(by: { (webFeed1, webFeed2) -> Bool in
if webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedSame {
return webFeed1.url < webFeed2.url
}
return webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedAscending
})
}
}

View File

@ -350,10 +350,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
@objc func webFeedSettingDidChange(_ note: Notification) { @objc func webFeedSettingDidChange(_ note: Notification) {
guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else { guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.WebFeedSettingUserInfoKey] as? String else {
return return
} }
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL { if key == Feed.WebFeedSettingKey.homePageURL || key == Feed.WebFeedSettingKey.faviconURL {
let _ = faviconDownloader.favicon(for: feed) let _ = faviconDownloader.favicon(for: feed)
} }
} }

View File

@ -20,7 +20,7 @@ final class WebFeedInspectorViewController: NSViewController, Inspector {
@IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton! @IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton!
@IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton? @IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton?
private var feed: WebFeed? { private var feed: Feed? {
didSet { didSet {
if feed != oldValue { if feed != oldValue {
updateUI() updateUI()
@ -42,7 +42,7 @@ final class WebFeedInspectorViewController: NSViewController, Inspector {
var windowTitle: String = NSLocalizedString("Feed Inspector", comment: "Feed Inspector window title") var windowTitle: String = NSLocalizedString("Feed Inspector", comment: "Feed Inspector window title")
func canInspect(_ objects: [Any]) -> Bool { func canInspect(_ objects: [Any]) -> Bool {
return objects.count == 1 && objects.first is WebFeed return objects.count == 1 && objects.first is Feed
} }
// MARK: NSViewController // MARK: NSViewController
@ -123,7 +123,7 @@ extension WebFeedInspectorViewController: NSTextFieldDelegate {
private extension WebFeedInspectorViewController { private extension WebFeedInspectorViewController {
func updateFeed() { func updateFeed() {
guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? WebFeed else { guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? Feed else {
feed = nil feed = nil
return return
} }

View File

@ -176,7 +176,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
} }
} }
if let feed = currentFeedOrFolder as? WebFeed, let noteObject = noteObject as? WebFeed { if let feed = currentFeedOrFolder as? Feed, let noteObject = noteObject as? Feed {
if feed == noteObject { if feed == noteObject {
updateWindowTitle() updateWindowTitle()
return return
@ -633,7 +633,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
detailViewController?.setState(detailState, mode: mode) detailViewController?.setState(detailState, mode: mode)
} }
func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: WebFeed) { func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: Feed) {
sidebarViewController?.selectFeed(webFeed) sidebarViewController?.selectFeed(webFeed)
} }

View File

@ -146,7 +146,7 @@ struct PasteboardWebFeed: Hashable {
} }
} }
extension WebFeed: @retroactive PasteboardWriterOwner { extension Feed: @retroactive PasteboardWriterOwner {
public var pasteboardWriter: NSPasteboardWriting { public var pasteboardWriter: NSPasteboardWriting {
return WebFeedPasteboardWriter(webFeed: self) return WebFeedPasteboardWriter(webFeed: self)
@ -155,14 +155,14 @@ extension WebFeed: @retroactive PasteboardWriterOwner {
@objc final class WebFeedPasteboardWriter: NSObject, NSPasteboardWriting { @objc final class WebFeedPasteboardWriter: NSObject, NSPasteboardWriting {
private let webFeed: WebFeed private let webFeed: Feed
static let webFeedUTI = "com.ranchero.webFeed" static let webFeedUTI = "com.ranchero.webFeed"
static let webFeedUTIType = NSPasteboard.PasteboardType(rawValue: webFeedUTI) static let webFeedUTIType = NSPasteboard.PasteboardType(rawValue: webFeedUTI)
static let webFeedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.webFeed" static let webFeedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.webFeed"
static let webFeedUTIInternalType = NSPasteboard.PasteboardType(rawValue: webFeedUTIInternal) static let webFeedUTIInternalType = NSPasteboard.PasteboardType(rawValue: webFeedUTIInternal)
init(webFeed: WebFeed) { init(webFeed: Feed) {
self.webFeed = webFeed self.webFeed = webFeed
} }

View File

@ -22,7 +22,7 @@ enum SidebarDeleteItemsAlert {
alert.messageText = NSLocalizedString("Delete Folder", comment: "Delete Folder") alert.messageText = NSLocalizedString("Delete Folder", comment: "Delete Folder")
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” folder?", comment: "Folder delete text") let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” folder?", comment: "Folder delete text")
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, folder.nameForDisplay) as String alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, folder.nameForDisplay) as String
} else if let feed = nodes.first?.representedObject as? Feed { } else if let feed = nodes.first?.representedObject as? SidebarItem {
alert.messageText = NSLocalizedString("Delete Feed", comment: "Delete Feed") alert.messageText = NSLocalizedString("Delete Feed", comment: "Delete Feed")
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text") let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text")
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String

View File

@ -136,7 +136,7 @@ private extension SidebarOutlineDataSource {
// Dont allow PseudoFeed to be dragged. // Dont allow PseudoFeed to be dragged.
// This will have to be revisited later. For instance, // This will have to be revisited later. For instance,
// user-created smart feeds should be draggable, maybe. // user-created smart feeds should be draggable, maybe.
return node.representedObject is Folder || node.representedObject is WebFeed return node.representedObject is Folder || node.representedObject is Feed
} }
// MARK: - Drag and Drop // MARK: - Drag and Drop
@ -249,7 +249,7 @@ private extension SidebarOutlineDataSource {
if let folder = node.representedObject as? Folder { if let folder = node.representedObject as? Folder {
return folder.account return folder.account
} }
if let feed = node.representedObject as? WebFeed { if let feed = node.representedObject as? Feed {
return feed.account return feed.account
} }
return nil return nil
@ -309,7 +309,7 @@ private extension SidebarOutlineDataSource {
} }
func copyWebFeedInAccount(node: Node, to parentNode: Node) { func copyWebFeedInAccount(node: Node, to parentNode: Node) {
guard let feed = node.representedObject as? WebFeed, let destination = parentNode.representedObject as? Container else { guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
return return
} }
@ -324,7 +324,7 @@ private extension SidebarOutlineDataSource {
} }
func moveWebFeedInAccount(node: Node, to parentNode: Node) { func moveWebFeedInAccount(node: Node, to parentNode: Node) {
guard let feed = node.representedObject as? WebFeed, guard let feed = node.representedObject as? Feed,
let source = node.parent?.representedObject as? Container, let source = node.parent?.representedObject as? Container,
let destination = parentNode.representedObject as? Container else { let destination = parentNode.representedObject as? Container else {
return return
@ -343,7 +343,7 @@ private extension SidebarOutlineDataSource {
} }
func copyWebFeedBetweenAccounts(node: Node, to parentNode: Node) { func copyWebFeedBetweenAccounts(node: Node, to parentNode: Node) {
guard let feed = node.representedObject as? WebFeed, guard let feed = node.representedObject as? Feed,
let destinationAccount = nodeAccount(parentNode), let destinationAccount = nodeAccount(parentNode),
let destinationContainer = parentNode.representedObject as? Container else { let destinationContainer = parentNode.representedObject as? Container else {
return return
@ -495,7 +495,7 @@ private extension SidebarOutlineDataSource {
} }
func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool { func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
guard let feed = node.representedObject as? WebFeed else { guard let feed = node.representedObject as? Feed else {
return false return false
} }
for draggedFeed in draggedFeeds { for draggedFeed in draggedFeeds {
@ -520,7 +520,7 @@ private extension SidebarOutlineDataSource {
return account return account
} else if let folder = node.representedObject as? Folder { } else if let folder = node.representedObject as? Folder {
return folder.account return folder.account
} else if let webFeed = node.representedObject as? WebFeed { } else if let webFeed = node.representedObject as? Feed {
return webFeed.account return webFeed.account
} else { } else {
return nil return nil

View File

@ -31,8 +31,8 @@ extension SidebarViewController {
let object = objects.first! let object = objects.first!
switch object { switch object {
case is WebFeed: case is Feed:
return menuForWebFeed(object as! WebFeed) return menuForWebFeed(object as! Feed)
case is Folder: case is Folder:
return menuForFolder(object as! Folder) return menuForFolder(object as! Folder)
case is PseudoFeed: case is PseudoFeed:
@ -93,7 +93,7 @@ extension SidebarViewController {
@objc func renameFromContextualMenu(_ sender: Any?) { @objc func renameFromContextualMenu(_ sender: Any?) {
guard let window = view.window, let menuItem = sender as? NSMenuItem, let object = menuItem.representedObject as? DisplayNameProvider, object is WebFeed || object is Folder else { guard let window = view.window, let menuItem = sender as? NSMenuItem, let object = menuItem.representedObject as? DisplayNameProvider, object is Feed || object is Folder else {
return return
} }
@ -106,7 +106,7 @@ extension SidebarViewController {
@objc func toggleNotificationsFromContextMenu(_ sender: Any?) { @objc func toggleNotificationsFromContextMenu(_ sender: Any?) {
guard let item = sender as? NSMenuItem, guard let item = sender as? NSMenuItem,
let feed = item.representedObject as? WebFeed else { let feed = item.representedObject as? Feed else {
return return
} }
UNUserNotificationCenter.current().getNotificationSettings { (settings) in UNUserNotificationCenter.current().getNotificationSettings { (settings) in
@ -137,7 +137,7 @@ extension SidebarViewController {
@objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) { @objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) {
guard let item = sender as? NSMenuItem, guard let item = sender as? NSMenuItem,
let feed = item.representedObject as? WebFeed else { let feed = item.representedObject as? Feed else {
return return
} }
if feed.isArticleExtractorAlwaysOn == nil { feed.isArticleExtractorAlwaysOn = false } if feed.isArticleExtractorAlwaysOn == nil { feed.isArticleExtractorAlwaysOn = false }
@ -170,7 +170,7 @@ extension SidebarViewController: RenameWindowControllerDelegate {
func renameWindowController(_ windowController: RenameWindowController, didRenameObject object: Any, withNewName name: String) { func renameWindowController(_ windowController: RenameWindowController, didRenameObject object: Any, withNewName name: String) {
if let feed = object as? WebFeed { if let feed = object as? Feed {
feed.rename(to: name) { result in feed.rename(to: name) { result in
switch result { switch result {
case .success: case .success:
@ -206,7 +206,7 @@ private extension SidebarViewController {
return menu return menu
} }
func menuForWebFeed(_ webFeed: WebFeed) -> NSMenu? { func menuForWebFeed(_ webFeed: Feed) -> NSMenu? {
let menu = NSMenu(title: "") let menu = NSMenu(title: "")
@ -338,7 +338,7 @@ private extension SidebarViewController {
func objectIsFeedOrFolder(_ object: Any) -> Bool { func objectIsFeedOrFolder(_ object: Any) -> Bool {
return object is WebFeed || object is Folder return object is Feed || object is Folder
} }
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem { func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {

View File

@ -97,7 +97,7 @@ protocol SidebarDelegate: AnyObject {
func saveState(to state: inout [AnyHashable : Any]) { func saveState(to state: inout [AnyHashable : Any]) {
state[UserInfoKey.readFeedsFilterState] = isReadFiltered state[UserInfoKey.readFeedsFilterState] = isReadFiltered
state[UserInfoKey.containerExpandedWindowState] = expandedTable.map { $0.userInfo } state[UserInfoKey.containerExpandedWindowState] = expandedTable.map { $0.userInfo }
state[UserInfoKey.selectedFeedsState] = selectedFeeds.compactMap { $0.feedID?.userInfo } state[UserInfoKey.selectedFeedsState] = selectedFeeds.compactMap { $0.sidebarItemID?.userInfo }
} }
func restoreState(from state: [AnyHashable : Any]) { func restoreState(from state: [AnyHashable : Any]) {
@ -111,7 +111,7 @@ protocol SidebarDelegate: AnyObject {
return return
} }
let selectedFeedIdentifers = Set(selectedFeedsState.compactMap( { FeedIdentifier(userInfo: $0) })) let selectedFeedIdentifers = Set(selectedFeedsState.compactMap( { SidebarItemIdentifier(userInfo: $0) }))
selectedFeedIdentifers.forEach { treeControllerDelegate.addFilterException($0) } selectedFeedIdentifers.forEach { treeControllerDelegate.addFilterException($0) }
rebuildTreeAndReloadDataIfNeeded() rebuildTreeAndReloadDataIfNeeded()
@ -119,7 +119,7 @@ protocol SidebarDelegate: AnyObject {
var selectIndexes = IndexSet() var selectIndexes = IndexSet()
func selectFeedsVisitor(node: Node) { func selectFeedsVisitor(node: Node) {
if let feedID = (node.representedObject as? FeedIdentifiable)?.feedID { if let feedID = (node.representedObject as? SidebarItemIdentifiable)?.sidebarItemID {
if selectedFeedIdentifers.contains(feedID) { if selectedFeedIdentifers.contains(feedID) {
selectIndexes.insert(outlineView.row(forItem: node) ) selectIndexes.insert(outlineView.row(forItem: node) )
} }
@ -194,15 +194,15 @@ protocol SidebarDelegate: AnyObject {
} }
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) { @objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else { return } guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? Feed else { return }
configureCellsForRepresentedObject(webFeed) configureCellsForRepresentedObject(webFeed)
} }
@objc func webFeedSettingDidChange(_ note: Notification) { @objc func webFeedSettingDidChange(_ note: Notification) {
guard let webFeed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else { guard let webFeed = note.object as? Feed, let key = note.userInfo?[Feed.WebFeedSettingUserInfoKey] as? String else {
return return
} }
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL { if key == Feed.WebFeedSettingKey.homePageURL || key == Feed.WebFeedSettingKey.faviconURL {
configureCellsForRepresentedObject(webFeed) configureCellsForRepresentedObject(webFeed)
} }
} }
@ -444,13 +444,13 @@ protocol SidebarDelegate: AnyObject {
// MARK: - API // MARK: - API
func selectFeed(_ feed: Feed) { func selectFeed(_ feed: SidebarItem) {
if isReadFiltered, let feedID = feed.feedID { if isReadFiltered, let feedID = feed.sidebarItemID {
self.treeControllerDelegate.addFilterException(feedID) self.treeControllerDelegate.addFilterException(feedID)
if let webFeed = feed as? WebFeed, let account = webFeed.account { if let webFeed = feed as? Feed, let account = webFeed.account {
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) }) let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
if let parentFolderFeedID = parentFolder?.feedID { if let parentFolderFeedID = parentFolder?.sidebarItemID {
self.treeControllerDelegate.addFilterException(parentFolderFeedID) self.treeControllerDelegate.addFilterException(parentFolderFeedID)
} }
} }
@ -465,7 +465,7 @@ protocol SidebarDelegate: AnyObject {
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) { func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
guard let accountNode = findAccountNode(userInfo), guard let accountNode = findAccountNode(userInfo),
let feedNode = findFeedNode(userInfo, beginningAt: accountNode), let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
let feed = feedNode.representedObject as? Feed else { let feed = feedNode.representedObject as? SidebarItem else {
return return
} }
selectFeed(feed) selectFeed(feed)
@ -510,8 +510,8 @@ private extension SidebarViewController {
return [Node]() return [Node]()
} }
var selectedFeeds: [Feed] { var selectedFeeds: [SidebarItem] {
selectedNodes.compactMap { $0.representedObject as? Feed } selectedNodes.compactMap { $0.representedObject as? SidebarItem }
} }
var singleSelectedNode: Node? { var singleSelectedNode: Node? {
@ -521,26 +521,26 @@ private extension SidebarViewController {
return selectedNodes.first! return selectedNodes.first!
} }
var singleSelectedWebFeed: WebFeed? { var singleSelectedWebFeed: Feed? {
guard let node = singleSelectedNode else { guard let node = singleSelectedNode else {
return nil return nil
} }
return node.representedObject as? WebFeed return node.representedObject as? Feed
} }
func addAllSelectedToFilterExceptions() { func addAllSelectedToFilterExceptions() {
selectedFeeds.forEach { addToFilterExeptionsIfNecessary($0) } selectedFeeds.forEach { addToFilterExeptionsIfNecessary($0) }
} }
func addToFilterExeptionsIfNecessary(_ feed: Feed?) { func addToFilterExeptionsIfNecessary(_ feed: SidebarItem?) {
if isReadFiltered, let feedID = feed?.feedID { if isReadFiltered, let feedID = feed?.sidebarItemID {
if feed is PseudoFeed { if feed is PseudoFeed {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} else if let folderFeed = feed as? Folder { } else if let folderFeed = feed as? Folder {
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil { if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} }
} else if let webFeed = feed as? WebFeed { } else if let webFeed = feed as? Feed {
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil { if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
addParentFolderToFilterExceptions(webFeed) addParentFolderToFilterExceptions(webFeed)
@ -549,10 +549,10 @@ private extension SidebarViewController {
} }
} }
func addParentFolderToFilterExceptions(_ feed: Feed) { func addParentFolderToFilterExceptions(_ feed: SidebarItem) {
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject), guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
let folder = node.parent?.representedObject as? Folder, let folder = node.parent?.representedObject as? Folder,
let folderFeedID = folder.feedID else { let folderFeedID = folder.sidebarItemID else {
return return
} }
@ -610,7 +610,7 @@ private extension SidebarViewController {
} }
func addTreeControllerToFilterExceptionsVisitor(node: Node) { func addTreeControllerToFilterExceptionsVisitor(node: Node) {
if let feed = node.representedObject as? Feed, let feedID = feed.feedID { if let feed = node.representedObject as? SidebarItem, let feedID = feed.sidebarItemID {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} }
} }
@ -741,7 +741,7 @@ private extension SidebarViewController {
guard let webFeedID = userInfo?[ArticlePathKey.webFeedID] as? String else { guard let webFeedID = userInfo?[ArticlePathKey.webFeedID] as? String else {
return nil return nil
} }
if let node = startingNode.descendantNode(where: { ($0.representedObject as? WebFeed)?.webFeedID == webFeedID }) { if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.webFeedID == webFeedID }) {
return node return node
} }
return nil return nil
@ -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 = IconImageCache.shared.imageForFeed(feed) { if let feed = node.representedObject as? Feed, let feedIcon = IconImageCache.shared.imageForFeed(feed) {
return feedIcon return feedIcon
} }
if let smallIconProvider = node.representedObject as? SmallIconProvider { if let smallIconProvider = node.representedObject as? SmallIconProvider {
@ -858,7 +858,7 @@ private extension Node {
if representedObject === object { if representedObject === object {
return true return true
} }
if let feed1 = object as? WebFeed, let feed2 = representedObject as? WebFeed { if let feed1 = object as? Feed, let feed2 = representedObject as? Feed {
return feed1 == feed2 return feed1 == feed2
} }
return false return false

View File

@ -12,7 +12,7 @@ import Articles
protocol TimelineContainerViewControllerDelegate: AnyObject { protocol TimelineContainerViewControllerDelegate: AnyObject {
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: WebFeed) func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: Feed)
func timelineInvalidatedRestorationState(_: TimelineContainerViewController) func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
} }
@ -141,7 +141,7 @@ extension TimelineContainerViewController: TimelineDelegate {
delegate?.timelineSelectionDidChange(self, articles: selectedArticles, mode: mode(for: timelineViewController)) delegate?.timelineSelectionDidChange(self, articles: selectedArticles, mode: mode(for: timelineViewController))
} }
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: WebFeed) { func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: Feed) {
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed) delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
} }

View File

@ -65,7 +65,7 @@ extension TimelineViewController {
} }
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) { @objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let webFeed = menuItem.representedObject as? WebFeed else { guard let menuItem = sender as? NSMenuItem, let webFeed = menuItem.representedObject as? Feed else {
return return
} }
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed) delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
@ -164,7 +164,7 @@ private extension TimelineViewController {
menu.addSeparatorIfNeeded() menu.addSeparatorIfNeeded()
if articles.count == 1, let feed = articles.first!.webFeed { if articles.count == 1, let feed = articles.first!.webFeed {
if !(representedObjects?.contains(where: { $0 as? WebFeed == feed }) ?? false) { if !(representedObjects?.contains(where: { $0 as? Feed == feed }) ?? false) {
menu.addItem(selectFeedInSidebarMenuItem(feed)) menu.addItem(selectFeedInSidebarMenuItem(feed))
} }
if let markAllMenuItem = markAllAsReadMenuItem(feed) { if let markAllMenuItem = markAllAsReadMenuItem(feed) {
@ -248,13 +248,13 @@ private extension TimelineViewController {
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles) return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
} }
func selectFeedInSidebarMenuItem(_ feed: WebFeed) -> NSMenuItem { func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command") let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command")
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)
return menuItem(formattedMenuText as String, #selector(selectFeedInSidebarFromContextualMenu(_:)), feed) return menuItem(formattedMenuText as String, #selector(selectFeedInSidebarFromContextualMenu(_:)), feed)
} }
func markAllAsReadMenuItem(_ feed: WebFeed) -> NSMenuItem? { func markAllAsReadMenuItem(_ feed: Feed) -> NSMenuItem? {
guard let articlesSet = try? feed.fetchArticles() else { guard let articlesSet = try? feed.fetchArticles() else {
return nil return nil
} }

View File

@ -14,7 +14,7 @@ import os.log
protocol TimelineDelegate: AnyObject { protocol TimelineDelegate: AnyObject {
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?) func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: WebFeed) func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: Feed)
func timelineInvalidatedRestorationState(_: TimelineViewController) func timelineInvalidatedRestorationState(_: TimelineViewController)
} }
@ -28,15 +28,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
@IBOutlet var tableView: TimelineTableView! @IBOutlet var tableView: TimelineTableView!
private var readFilterEnabledTable = [FeedIdentifier: Bool]() private var readFilterEnabledTable = [SidebarItemIdentifier: Bool]()
var isReadFiltered: Bool? { var isReadFiltered: Bool? {
guard representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? Feed else { guard representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem else {
return nil return nil
} }
guard timelineFeed.defaultReadFilterType != .alwaysRead else { guard timelineFeed.defaultReadFilterType != .alwaysRead else {
return nil return nil
} }
if let feedID = timelineFeed.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] { if let feedID = timelineFeed.sidebarItemID, let readFilterEnabled = readFilterEnabledTable[feedID] {
return readFilterEnabled return readFilterEnabled
} else { } else {
return timelineFeed.defaultReadFilterType == .read return timelineFeed.defaultReadFilterType == .read
@ -46,7 +46,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
var isCleanUpAvailable: Bool { var isCleanUpAvailable: Bool {
let isEligibleForCleanUp: Bool? let isEligibleForCleanUp: Bool?
if representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? Feed, timelineFeed.defaultReadFilterType == .alwaysRead { if representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem, timelineFeed.defaultReadFilterType == .alwaysRead {
isEligibleForCleanUp = true isEligibleForCleanUp = true
} else { } else {
isEligibleForCleanUp = isReadFiltered isEligibleForCleanUp = isReadFiltered
@ -111,7 +111,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return return
} }
if let representedObjects = representedObjects, representedObjects.count == 1 && representedObjects.first is WebFeed { if let representedObjects = representedObjects, representedObjects.count == 1 && representedObjects.first is Feed {
showFeedNames = { showFeedNames = {
for article in articles { for article in articles {
if !article.byline().isEmpty { if !article.byline().isEmpty {
@ -263,7 +263,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
func toggleReadFilter() { func toggleReadFilter() {
guard let filter = isReadFiltered, let feedID = (representedObjects?.first as? Feed)?.feedID else { return } guard let filter = isReadFiltered, let feedID = (representedObjects?.first as? SidebarItem)?.sidebarItemID else { return }
readFilterEnabledTable[feedID] = !filter readFilterEnabledTable[feedID] = !filter
delegate?.timelineInvalidatedRestorationState(self) delegate?.timelineInvalidatedRestorationState(self)
fetchAndReplacePreservingSelection() fetchAndReplacePreservingSelection()
@ -287,7 +287,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
for i in 0..<readArticlesFilterStateKeys.count { for i in 0..<readArticlesFilterStateKeys.count {
if let feedIdentifier = FeedIdentifier(userInfo: readArticlesFilterStateKeys[i]) { if let feedIdentifier = SidebarItemIdentifier(userInfo: readArticlesFilterStateKeys[i]) {
readFilterEnabledTable[feedIdentifier] = readArticlesFilterStateValues[i] readFilterEnabledTable[feedIdentifier] = readArticlesFilterStateValues[i]
} }
} }
@ -594,7 +594,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) { @objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
guard showIcons, let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else { guard showIcons, let feed = note.userInfo?[UserInfoKey.webFeed] as? Feed else {
return return
} }
let indexesToReload = tableView.indexesOfAvailableRowsPassingTest { (row) -> Bool in let indexesToReload = tableView.indexesOfAvailableRowsPassingTest { (row) -> Bool in
@ -636,7 +636,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
@objc func accountDidDownloadArticles(_ note: Notification) { @objc func accountDidDownloadArticles(_ note: Notification) {
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else { guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<Feed> else {
return return
} }
@ -1151,7 +1151,7 @@ private extension TimelineViewController {
var fetchedArticles = Set<Article>() var fetchedArticles = Set<Article>()
for fetchers in fetchers { for fetchers in fetchers {
if (fetchers as? Feed)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true { if (fetchers as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true {
if let articles = try? fetchers.fetchUnreadArticles() { if let articles = try? fetchers.fetchUnreadArticles() {
fetchedArticles.formUnion(articles) fetchedArticles.formUnion(articles)
} }
@ -1226,14 +1226,14 @@ private extension TimelineViewController {
return representedObjects?.contains(where: { $0 is Folder }) ?? false return representedObjects?.contains(where: { $0 is Folder }) ?? false
} }
func representedObjectsContainsAnyWebFeed(_ webFeeds: Set<WebFeed>) -> Bool { func representedObjectsContainsAnyWebFeed(_ webFeeds: Set<Feed>) -> Bool {
// Return true if theres a match or if a folder contains (recursively) one of feeds // Return true if theres a match or if a folder contains (recursively) one of feeds
guard let representedObjects = representedObjects else { guard let representedObjects = representedObjects else {
return false return false
} }
for representedObject in representedObjects { for representedObject in representedObjects {
if let feed = representedObject as? WebFeed { if let feed = representedObject as? Feed {
for oneFeed in webFeeds { for oneFeed in webFeeds {
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url { if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
return true return true

View File

@ -73,10 +73,10 @@ extension NSApplication : ScriptingObjectContainer {
for 'articles of feed "The Shape of Everything" of account "On My Mac"' for 'articles of feed "The Shape of Everything" of account "On My Mac"'
*/ */
func allWebFeeds() -> [WebFeed] { func allWebFeeds() -> [Feed] {
let accounts = AccountManager.shared.activeAccounts let accounts = AccountManager.shared.activeAccounts
let emptyFeeds:[WebFeed] = [] let emptyFeeds:[Feed] = []
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [WebFeed] in return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in
let accountFeeds = Array(nthAccount.topLevelWebFeeds) let accountFeeds = Array(nthAccount.topLevelWebFeeds)
return result + accountFeeds return result + accountFeeds
} }

View File

@ -14,10 +14,10 @@ import Articles
@objc(ScriptableWebFeed) @objc(ScriptableWebFeed)
class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let webFeed:WebFeed let webFeed:Feed
let container:ScriptingObjectContainer let container:ScriptingObjectContainer
init (_ webFeed:WebFeed, container:ScriptingObjectContainer) { init (_ webFeed:Feed, container:ScriptingObjectContainer) {
self.webFeed = webFeed self.webFeed = webFeed
self.container = container self.container = container
} }
@ -71,7 +71,7 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
return url return url
} }
class func scriptableFeed(_ feed:WebFeed, account:Account, folder:Folder?) -> ScriptableWebFeed { class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableWebFeed {
let scriptableAccount = ScriptableAccount(account) let scriptableAccount = ScriptableAccount(account)
if let folder = folder { if let folder = folder {
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)

View File

@ -48,12 +48,12 @@ class ActivityManager {
invalidateNextUnread() invalidateNextUnread()
} }
func selecting(feed: Feed) { func selecting(feed: SidebarItem) {
invalidateCurrentActivities() invalidateCurrentActivities()
selectingActivity = makeSelectFeedActivity(feed: feed) selectingActivity = makeSelectFeedActivity(feed: feed)
if let webFeed = feed as? WebFeed { if let webFeed = feed as? Feed {
updateSelectingActivityFeedSearchAttributes(with: webFeed) updateSelectingActivityFeedSearchAttributes(with: webFeed)
} }
@ -86,7 +86,7 @@ class ActivityManager {
nextUnreadActivity = nil nextUnreadActivity = nil
} }
func reading(feed: Feed?, article: Article?) { func reading(feed: SidebarItem?, article: Article?) {
invalidateReading() invalidateReading()
invalidateNextUnread() invalidateNextUnread()
@ -134,13 +134,13 @@ class ActivityManager {
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
} }
static func cleanUp(_ webFeed: WebFeed) { static func cleanUp(_ webFeed: Feed) {
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: webFeed)) CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: webFeed))
} }
#endif #endif
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) { @objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.webFeedID] as? String else { guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.webFeedID] as? String else {
return return
} }
@ -161,7 +161,7 @@ class ActivityManager {
private extension ActivityManager { private extension ActivityManager {
func makeSelectFeedActivity(feed: Feed) -> NSUserActivity { func makeSelectFeedActivity(feed: SidebarItem) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue) let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue)
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder") let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder")
@ -171,27 +171,27 @@ private extension ActivityManager {
activity.keywords = Set(makeKeywords(title)) activity.keywords = Set(makeKeywords(title))
activity.isEligibleForSearch = true activity.isEligibleForSearch = true
let articleFetcherIdentifierUserInfo = feed.feedID?.userInfo ?? [AnyHashable: Any]() let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]()
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo] activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo]
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String }) activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
activity.persistentIdentifier = feed.feedID?.description ?? "" activity.persistentIdentifier = feed.sidebarItemID?.description ?? ""
#if os(iOS) #if os(iOS)
activity.suggestedInvocationPhrase = title activity.suggestedInvocationPhrase = title
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.contentAttributeSet?.relatedUniqueIdentifier = feed.feedID?.description ?? "" activity.contentAttributeSet?.relatedUniqueIdentifier = feed.sidebarItemID?.description ?? ""
#endif #endif
return activity return activity
} }
func makeReadArticleActivity(feed: Feed?, article: Article) -> NSUserActivity { func makeReadArticleActivity(feed: SidebarItem?, article: Article) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue) let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
activity.title = ArticleStringFormatter.truncatedTitle(article) activity.title = ArticleStringFormatter.truncatedTitle(article)
if let feed = feed { if let feed = feed {
let articleFetcherIdentifierUserInfo = feed.feedID?.userInfo ?? [AnyHashable: Any]() let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]()
let articlePathUserInfo = article.pathUserInfo let articlePathUserInfo = article.pathUserInfo
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo] activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo]
} else { } else {
@ -244,7 +244,7 @@ private extension ActivityManager {
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? [] return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
} }
func updateSelectingActivityFeedSearchAttributes(with feed: WebFeed) { func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String) let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
attributeSet.title = feed.nameForDisplay attributeSet.title = feed.nameForDisplay
@ -277,7 +277,7 @@ private extension ActivityManager {
return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)" return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
} }
static func identifier(for feed: WebFeed) -> String { static func identifier(for feed: Feed) -> String {
return "account_\(feed.account!.accountID)_feed_\(feed.webFeedID)" return "account_\(feed.account!.accountID)_feed_\(feed.webFeedID)"
} }
@ -285,7 +285,7 @@ private extension ActivityManager {
return "account_\(article.accountID)_feed_\(article.webFeedID)_article_\(article.articleID)" return "account_\(article.accountID)_feed_\(article.webFeedID)_article_\(article.articleID)"
} }
static func identifiers(for feed: WebFeed) -> [String] { static func identifiers(for feed: Feed) -> [String] {
var ids = [String]() var ids = [String]()
ids.append(identifier(for: feed)) ids.append(identifier(for: feed))
if let articles = try? feed.fetchArticles() { if let articles = try? feed.fetchArticles() {

View File

@ -77,7 +77,7 @@ final class DeleteCommand: UndoableCommand {
} }
for node in nodes { for node in nodes {
if let _ = node.representedObject as? WebFeed { if let _ = node.representedObject as? Feed {
continue continue
} }
if let _ = node.representedObject as? Folder { if let _ = node.representedObject as? Folder {
@ -98,7 +98,7 @@ private struct SidebarItemSpecifier {
private weak var account: Account? private weak var account: Account?
private let parentFolder: Folder? private let parentFolder: Folder?
private let folder: Folder? private let folder: Folder?
private let webFeed: WebFeed? private let webFeed: Feed?
private let path: ContainerPath private let path: ContainerPath
private let errorHandler: (Error) -> () private let errorHandler: (Error) -> ()
@ -118,7 +118,7 @@ private struct SidebarItemSpecifier {
self.parentFolder = node.parentFolder() self.parentFolder = node.parentFolder()
if let webFeed = node.representedObject as? WebFeed { if let webFeed = node.representedObject as? Feed {
self.webFeed = webFeed self.webFeed = webFeed
self.folder = nil self.folder = nil
account = webFeed.account account = webFeed.account
@ -271,7 +271,7 @@ private struct DeleteActionName {
var numberOfFolders = 0 var numberOfFolders = 0
for node in nodes { for node in nodes {
if let _ = node.representedObject as? WebFeed { if let _ = node.representedObject as? Feed {
numberOfFeeds += 1 numberOfFeeds += 1
} }
else if let _ = node.representedObject as? Folder { else if let _ = node.representedObject as? Folder {

View File

@ -42,7 +42,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
extension Article { extension Article {
var webFeed: WebFeed? { var webFeed: Feed? {
return account?.existingWebFeed(withWebFeedID: webFeedID) return account?.existingWebFeed(withWebFeedID: webFeedID)
} }
@ -121,7 +121,7 @@ extension Article {
return IconImageCache.shared.imageForArticle(self) return IconImageCache.shared.imageForArticle(self)
} }
func iconImageUrl(webFeed: WebFeed) -> URL? { func iconImageUrl(webFeed: Feed) -> URL? {
if let image = iconImage() { if let image = iconImage() {
let fm = FileManager.default let fm = FileManager.default
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0] var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]

View File

@ -25,7 +25,7 @@ extension Account: SmallIconProvider {
} }
} }
extension WebFeed: SmallIconProvider { extension Feed: SmallIconProvider {
var smallIcon: IconImage? { var smallIcon: IconImage? {
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) { if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {

View File

@ -44,7 +44,7 @@ final class FaviconDownloader {
} }
private let queue: DispatchQueue private let queue: DispatchQueue
private var cache = [WebFeed: IconImage]() // faviconURL: RSImage private var cache = [Feed: IconImage]() // faviconURL: RSImage
struct UserInfoKey { struct UserInfoKey {
static let faviconURL = "faviconURL" static let faviconURL = "faviconURL"
@ -69,10 +69,10 @@ final class FaviconDownloader {
// MARK: - API // MARK: - API
func resetCache() { func resetCache() {
cache = [WebFeed: IconImage]() cache = [Feed: IconImage]()
} }
func favicon(for webFeed: WebFeed) -> IconImage? { func favicon(for webFeed: Feed) -> IconImage? {
assert(Thread.isMainThread) assert(Thread.isMainThread)
@ -94,7 +94,7 @@ final class FaviconDownloader {
return nil return nil
} }
func faviconAsIcon(for webFeed: WebFeed) -> IconImage? { func faviconAsIcon(for webFeed: Feed) -> IconImage? {
if let image = cache[webFeed] { if let image = cache[webFeed] {
return image return image

View File

@ -14,7 +14,7 @@ final class FaviconGenerator {
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage
static func favicon(_ webFeed: WebFeed) -> IconImage { static func favicon(_ webFeed: Feed) -> IconImage {
if let favicon = FaviconGenerator.faviconGeneratorCache[webFeed.url] { if let favicon = FaviconGenerator.faviconGeneratorCache[webFeed.url] {
return favicon return favicon

View File

@ -14,13 +14,13 @@ class IconImageCache {
static var shared = IconImageCache() static var shared = IconImageCache()
private var smartFeedIconImageCache = [FeedIdentifier: IconImage]() private var smartFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
private var webFeedIconImageCache = [FeedIdentifier: IconImage]() private var webFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
private var faviconImageCache = [FeedIdentifier: IconImage]() private var faviconImageCache = [SidebarItemIdentifier: IconImage]()
private var smallIconImageCache = [FeedIdentifier: IconImage]() private var smallIconImageCache = [SidebarItemIdentifier: IconImage]()
private var authorIconImageCache = [Author: IconImage]() private var authorIconImageCache = [Author: IconImage]()
func imageFor(_ feedID: FeedIdentifier) -> IconImage? { func imageFor(_ feedID: SidebarItemIdentifier) -> IconImage? {
if let smartFeed = SmartFeedsController.shared.find(by: feedID) { if let smartFeed = SmartFeedsController.shared.find(by: feedID) {
return imageForFeed(smartFeed) return imageForFeed(smartFeed)
} }
@ -30,15 +30,15 @@ class IconImageCache {
return nil return nil
} }
func imageForFeed(_ feed: Feed) -> IconImage? { func imageForFeed(_ feed: SidebarItem) -> IconImage? {
guard let feedID = feed.feedID else { guard let feedID = feed.sidebarItemID else {
return nil return nil
} }
if let smartFeed = feed as? PseudoFeed { if let smartFeed = feed as? PseudoFeed {
return imageForSmartFeed(smartFeed, feedID) return imageForSmartFeed(smartFeed, feedID)
} }
if let webFeed = feed as? WebFeed, let iconImage = imageForWebFeed(webFeed, feedID) { if let webFeed = feed as? Feed, let iconImage = imageForWebFeed(webFeed, feedID) {
return iconImage return iconImage
} }
if let smallIconProvider = feed as? SmallIconProvider { if let smallIconProvider = feed as? SmallIconProvider {
@ -59,17 +59,17 @@ class IconImageCache {
} }
func emptyCache() { func emptyCache() {
smartFeedIconImageCache = [FeedIdentifier: IconImage]() smartFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
webFeedIconImageCache = [FeedIdentifier: IconImage]() webFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
faviconImageCache = [FeedIdentifier: IconImage]() faviconImageCache = [SidebarItemIdentifier: IconImage]()
smallIconImageCache = [FeedIdentifier: IconImage]() smallIconImageCache = [SidebarItemIdentifier: IconImage]()
authorIconImageCache = [Author: IconImage]() authorIconImageCache = [Author: IconImage]()
} }
} }
private extension IconImageCache { private extension IconImageCache {
func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: FeedIdentifier) -> IconImage? { func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: SidebarItemIdentifier) -> IconImage? {
if let iconImage = smartFeedIconImageCache[feedID] { if let iconImage = smartFeedIconImageCache[feedID] {
return iconImage return iconImage
} }
@ -80,7 +80,7 @@ private extension IconImageCache {
return nil return nil
} }
func imageForWebFeed(_ webFeed: WebFeed, _ feedID: FeedIdentifier) -> IconImage? { func imageForWebFeed(_ webFeed: Feed, _ feedID: SidebarItemIdentifier) -> IconImage? {
if let iconImage = webFeedIconImageCache[feedID] { if let iconImage = webFeedIconImageCache[feedID] {
return iconImage return iconImage
} }
@ -98,7 +98,7 @@ private extension IconImageCache {
return nil return nil
} }
func imageForSmallIconProvider(_ provider: SmallIconProvider, _ feedID: FeedIdentifier) -> IconImage? { func imageForSmallIconProvider(_ provider: SmallIconProvider, _ feedID: SidebarItemIdentifier) -> IconImage? {
if let iconImage = smallIconImageCache[feedID] { if let iconImage = smallIconImageCache[feedID] {
return iconImage return iconImage
} }

View File

@ -53,8 +53,8 @@ public final class WebFeedIconDownloader {
}() }()
private var urlsInProgress = Set<String>() private var urlsInProgress = Set<String>()
private var cache = [WebFeed: IconImage]() private var cache = [Feed: IconImage]()
private var waitingForFeedURLs = [String: WebFeed]() private var waitingForFeedURLs = [String: Feed]()
init(imageDownloader: ImageDownloader, folder: String) { init(imageDownloader: ImageDownloader, folder: String) {
self.imageDownloader = imageDownloader self.imageDownloader = imageDownloader
@ -68,10 +68,10 @@ public final class WebFeedIconDownloader {
} }
func resetCache() { func resetCache() {
cache = [WebFeed: IconImage]() cache = [Feed: IconImage]()
} }
func icon(for feed: WebFeed) -> IconImage? { func icon(for feed: Feed) -> IconImage? {
if let cachedImage = cache[feed] { if let cachedImage = cache[feed] {
return cachedImage return cachedImage
@ -153,7 +153,7 @@ public final class WebFeedIconDownloader {
private extension WebFeedIconDownloader { private extension WebFeedIconDownloader {
func icon(forHomePageURL homePageURL: String, feed: WebFeed, _ imageResultBlock: @escaping (RSImage?) -> Void) { func icon(forHomePageURL homePageURL: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) {
if homePagesWithNoIconURLCache.contains(homePageURL) || homePagesWithUglyIcons.contains(homePageURL) { if homePagesWithNoIconURLCache.contains(homePageURL) || homePagesWithUglyIcons.contains(homePageURL) {
imageResultBlock(nil) imageResultBlock(nil)
@ -168,7 +168,7 @@ private extension WebFeedIconDownloader {
findIconURLForHomePageURL(homePageURL, feed: feed) findIconURLForHomePageURL(homePageURL, feed: feed)
} }
func icon(forURL url: String, feed: WebFeed, _ imageResultBlock: @escaping (RSImage?) -> Void) { func icon(forURL url: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) {
waitingForFeedURLs[url] = feed waitingForFeedURLs[url] = feed
guard let imageData = imageDownloader.image(for: url) else { guard let imageData = imageDownloader.image(for: url) else {
imageResultBlock(nil) imageResultBlock(nil)
@ -177,7 +177,7 @@ private extension WebFeedIconDownloader {
RSImage.scaledForIcon(imageData, imageResultBlock: imageResultBlock) RSImage.scaledForIcon(imageData, imageResultBlock: imageResultBlock)
} }
func postFeedIconDidBecomeAvailableNotification(_ feed: WebFeed) { func postFeedIconDidBecomeAvailableNotification(_ feed: Feed) {
DispatchQueue.main.async { DispatchQueue.main.async {
let userInfo: [AnyHashable: Any] = [UserInfoKey.webFeed: feed] let userInfo: [AnyHashable: Any] = [UserInfoKey.webFeed: feed]
@ -197,7 +197,7 @@ private extension WebFeedIconDownloader {
homePageToIconURLCacheDirty = true homePageToIconURLCacheDirty = true
} }
func findIconURLForHomePageURL(_ homePageURL: String, feed: WebFeed) { func findIconURLForHomePageURL(_ homePageURL: String, feed: Feed) {
guard !urlsInProgress.contains(homePageURL) else { guard !urlsInProgress.contains(homePageURL) else {
return return
@ -214,7 +214,7 @@ private extension WebFeedIconDownloader {
} }
} }
func pullIconURL(from metadata: RSHTMLMetadata, homePageURL: String, feed: WebFeed) { func pullIconURL(from metadata: RSHTMLMetadata, homePageURL: String, feed: Feed) {
if let url = metadata.bestWebsiteIconURL() { if let url = metadata.bestWebsiteIconURL() {
cacheIconURL(for: homePageURL, url) cacheIconURL(for: homePageURL, url)

View File

@ -13,7 +13,7 @@ import Articles
import Account import Account
import RSCore import RSCore
protocol PseudoFeed: AnyObject, Feed, SmallIconProvider, PasteboardWriterOwner { protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider, PasteboardWriterOwner {
} }
@ -24,7 +24,7 @@ import Articles
import Account import Account
import RSCore import RSCore
protocol PseudoFeed: AnyObject, Feed, SmallIconProvider { protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider {
} }

View File

@ -14,8 +14,8 @@ import ArticlesDatabase
struct SearchFeedDelegate: SmartFeedDelegate { struct SearchFeedDelegate: SmartFeedDelegate {
var feedID: FeedIdentifier? { var sidebarItemID: SidebarItemIdentifier? {
return FeedIdentifier.smartFeed(String(describing: SearchFeedDelegate.self)) return SidebarItemIdentifier.smartFeed(String(describing: SearchFeedDelegate.self))
} }
var nameForDisplay: String { var nameForDisplay: String {

View File

@ -14,8 +14,8 @@ import ArticlesDatabase
struct SearchTimelineFeedDelegate: SmartFeedDelegate { struct SearchTimelineFeedDelegate: SmartFeedDelegate {
var feedID: FeedIdentifier? { var sidebarItemID: SidebarItemIdentifier? {
return FeedIdentifier.smartFeed(String(describing: SearchTimelineFeedDelegate.self)) return SidebarItemIdentifier.smartFeed(String(describing: SearchTimelineFeedDelegate.self))
} }
var nameForDisplay: String { var nameForDisplay: String {

View File

@ -20,8 +20,8 @@ final class SmartFeed: PseudoFeed {
return .none return .none
} }
var feedID: FeedIdentifier? { var sidebarItemID: SidebarItemIdentifier? {
delegate.feedID delegate.sidebarItemID
} }
var nameForDisplay: String { var nameForDisplay: String {

View File

@ -12,7 +12,7 @@ import Articles
import ArticlesDatabase import ArticlesDatabase
import RSCore import RSCore
protocol SmartFeedDelegate: FeedIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider { protocol SmartFeedDelegate: SidebarItemIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider {
var fetchType: FetchType { get } var fetchType: FetchType { get }
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock)
} }

View File

@ -19,7 +19,7 @@ final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable {
public static let shared = SmartFeedsController() public static let shared = SmartFeedsController()
let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title") let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title")
var smartFeeds = [Feed]() var smartFeeds = [SidebarItem]()
let todayFeed = SmartFeed(delegate: TodayFeedDelegate()) let todayFeed = SmartFeed(delegate: TodayFeedDelegate())
let unreadFeed = UnreadFeed() let unreadFeed = UnreadFeed()
let starredFeed = SmartFeed(delegate: StarredFeedDelegate()) let starredFeed = SmartFeed(delegate: StarredFeedDelegate())
@ -28,7 +28,7 @@ final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable {
self.smartFeeds = [todayFeed, unreadFeed, starredFeed] self.smartFeeds = [todayFeed, unreadFeed, starredFeed]
} }
func find(by identifier: FeedIdentifier) -> PseudoFeed? { func find(by identifier: SidebarItemIdentifier) -> PseudoFeed? {
switch identifier { switch identifier {
case .smartFeed(let stringIdentifer): case .smartFeed(let stringIdentifer):
switch stringIdentifer { switch stringIdentifer {

View File

@ -16,8 +16,8 @@ import Account
struct StarredFeedDelegate: SmartFeedDelegate { struct StarredFeedDelegate: SmartFeedDelegate {
var feedID: FeedIdentifier? { var sidebarItemID: SidebarItemIdentifier? {
return FeedIdentifier.smartFeed(String(describing: StarredFeedDelegate.self)) return SidebarItemIdentifier.smartFeed(String(describing: StarredFeedDelegate.self))
} }
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title") let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")

View File

@ -14,8 +14,8 @@ import Account
struct TodayFeedDelegate: SmartFeedDelegate { struct TodayFeedDelegate: SmartFeedDelegate {
var feedID: FeedIdentifier? { var sidebarItemID: SidebarItemIdentifier? {
return FeedIdentifier.smartFeed(String(describing: TodayFeedDelegate.self)) return SidebarItemIdentifier.smartFeed(String(describing: TodayFeedDelegate.self))
} }
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title") let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")

View File

@ -26,8 +26,8 @@ final class UnreadFeed: PseudoFeed {
return .alwaysRead return .alwaysRead
} }
var feedID: FeedIdentifier? { var sidebarItemID: SidebarItemIdentifier? {
return FeedIdentifier.smartFeed(String(describing: UnreadFeed.self)) return SidebarItemIdentifier.smartFeed(String(describing: UnreadFeed.self))
} }
let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title") let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title")

View File

@ -19,13 +19,13 @@ typealias FetchRequestOperationResultBlock = (Set<Article>, FetchRequestOperatio
final class FetchRequestOperation { final class FetchRequestOperation {
let id: Int let id: Int
let readFilterEnabledTable: [FeedIdentifier: Bool] let readFilterEnabledTable: [SidebarItemIdentifier: Bool]
let resultBlock: FetchRequestOperationResultBlock let resultBlock: FetchRequestOperationResultBlock
var isCanceled = false var isCanceled = false
var isFinished = false var isFinished = false
private let fetchers: [ArticleFetcher] private let fetchers: [ArticleFetcher]
init(id: Int, readFilterEnabledTable: [FeedIdentifier: Bool], fetchers: [ArticleFetcher], resultBlock: @escaping FetchRequestOperationResultBlock) { init(id: Int, readFilterEnabledTable: [SidebarItemIdentifier: Bool], fetchers: [ArticleFetcher], resultBlock: @escaping FetchRequestOperationResultBlock) {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
self.id = id self.id = id
self.readFilterEnabledTable = readFilterEnabledTable self.readFilterEnabledTable = readFilterEnabledTable
@ -81,7 +81,7 @@ final class FetchRequestOperation {
} }
for fetcher in fetchers { for fetcher in fetchers {
if (fetcher as? Feed)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true { if (fetcher as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true {
fetcher.fetchUnreadArticlesAsync { articleSetResult in fetcher.fetchUnreadArticlesAsync { articleSetResult in
let articles = (try? articleSetResult.get()) ?? Set<Article>() let articles = (try? articleSetResult.get()) ?? Set<Article>()
process(articles) process(articles)

View File

@ -13,15 +13,15 @@ import Account
final class WebFeedTreeControllerDelegate: TreeControllerDelegate { final class WebFeedTreeControllerDelegate: TreeControllerDelegate {
private var filterExceptions = Set<FeedIdentifier>() private var filterExceptions = Set<SidebarItemIdentifier>()
var isReadFiltered = false var isReadFiltered = false
func addFilterException(_ feedID: FeedIdentifier) { func addFilterException(_ feedID: SidebarItemIdentifier) {
filterExceptions.insert(feedID) filterExceptions.insert(feedID)
} }
func resetFilterExceptions() { func resetFilterExceptions() {
filterExceptions = Set<FeedIdentifier>() filterExceptions = Set<SidebarItemIdentifier>()
} }
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
@ -67,14 +67,14 @@ private extension WebFeedTreeControllerDelegate {
var children = [AnyObject]() var children = [AnyObject]()
for webFeed in container.topLevelWebFeeds { for webFeed in container.topLevelWebFeeds {
if let feedID = webFeed.feedID, !(!filterExceptions.contains(feedID) && isReadFiltered && webFeed.unreadCount == 0) { if let feedID = webFeed.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && webFeed.unreadCount == 0) {
children.append(webFeed) children.append(webFeed)
} }
} }
if let folders = container.folders { if let folders = container.folders {
for folder in folders { for folder in folders {
if let feedID = folder.feedID, !(!filterExceptions.contains(feedID) && isReadFiltered && folder.unreadCount == 0) { if let feedID = folder.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && folder.unreadCount == 0) {
children.append(folder) children.append(folder)
} }
} }
@ -100,7 +100,7 @@ private extension WebFeedTreeControllerDelegate {
} }
func createNode(representedObject: Any, parent: Node) -> Node? { func createNode(representedObject: Any, parent: Node) -> Node? {
if let webFeed = representedObject as? WebFeed { if let webFeed = representedObject as? Feed {
return createNode(webFeed: webFeed, parent: parent) return createNode(webFeed: webFeed, parent: parent)
} }
@ -115,7 +115,7 @@ private extension WebFeedTreeControllerDelegate {
return nil return nil
} }
func createNode(webFeed: WebFeed, parent: Node) -> Node { func createNode(webFeed: Feed, parent: Node) -> Node {
return parent.createChildNode(webFeed) return parent.createChildNode(webFeed)
} }

View File

@ -53,7 +53,7 @@ final class UserNotificationManager: NSObject {
private extension UserNotificationManager { private extension UserNotificationManager {
func sendNotification(webFeed: WebFeed, article: Article) { func sendNotification(webFeed: Feed, article: Article) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = webFeed.nameForDisplay content.title = webFeed.nameForDisplay
@ -81,7 +81,7 @@ private extension UserNotificationManager {
/// - webFeed: `WebFeed` /// - webFeed: `WebFeed`
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil. /// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`. /// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
func thumbnailAttachment(for article: Article, webFeed: WebFeed) -> UNNotificationAttachment? { func thumbnailAttachment(for article: Article, webFeed: Feed) -> UNNotificationAttachment? {
if let imageURL = article.iconImageUrl(webFeed: webFeed) { if let imageURL = article.iconImageUrl(webFeed: webFeed) {
let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: imageURL, options: nil) let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: imageURL, options: nil)
return thumbnail return thumbnail

View File

@ -15,7 +15,7 @@ class WebFeedInspectorViewController: UITableViewController {
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0) static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
var webFeed: WebFeed! var webFeed: Feed!
@IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch! @IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch!
@IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch! @IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch!

View File

@ -13,7 +13,7 @@ import Account
extension MasterFeedViewController: UITableViewDragDelegate { extension MasterFeedViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let node = coordinator.nodeFor(indexPath), let webFeed = node.representedObject as? WebFeed else { guard let node = coordinator.nodeFor(indexPath), let webFeed = node.representedObject as? Feed else {
return [UIDragItem]() return [UIDragItem]()
} }

View File

@ -22,7 +22,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
return UITableViewDropProposal(operation: .forbidden) return UITableViewDropProposal(operation: .forbidden)
} }
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed, guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? SidebarItem,
let destAccount = destFeed.account, let destAccount = destFeed.account,
let destCell = tableView.cellForRow(at: destIndexPath) else { let destCell = tableView.cellForRow(at: destIndexPath) else {
return UITableViewDropProposal(operation: .forbidden) return UITableViewDropProposal(operation: .forbidden)
@ -31,7 +31,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
// Validate account specific behaviors... // Validate account specific behaviors...
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders), if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
let sourceNode = session.localDragSession?.items.first?.localObject as? Node, let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
let sourceWebFeed = sourceNode.representedObject as? WebFeed, let sourceWebFeed = sourceNode.representedObject as? Feed,
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) { sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
return UITableViewDropProposal(operation: .forbidden) return UITableViewDropProposal(operation: .forbidden)
} }
@ -91,7 +91,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
} }
}() }()
guard let destination = destinationContainer, let webFeed = dragNode.representedObject as? WebFeed else { return } guard let destination = destinationContainer, let webFeed = dragNode.representedObject as? Feed else { return }
if source.account == destination.account { if source.account == destination.account {
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination) moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
@ -100,7 +100,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
} }
} }
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { func moveWebFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
guard sourceContainer !== destinationContainer else { return } guard sourceContainer !== destinationContainer else { return }
BatchUpdate.shared.start() BatchUpdate.shared.start()
@ -115,7 +115,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
} }
} }
func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { func moveWebFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) { if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {

View File

@ -130,17 +130,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
} }
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) { @objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else { guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? Feed else {
return return
} }
applyToCellsForRepresentedObject(webFeed, configureIcon(_:_:)) applyToCellsForRepresentedObject(webFeed, configureIcon(_:_:))
} }
@objc func webFeedSettingDidChange(_ note: Notification) { @objc func webFeedSettingDidChange(_ note: Notification) {
guard let webFeed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else { guard let webFeed = note.object as? Feed, let key = note.userInfo?[Feed.WebFeedSettingUserInfoKey] as? String else {
return return
} }
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL { if key == Feed.WebFeedSettingKey.homePageURL || key == Feed.WebFeedSettingKey.faviconURL {
configureCellsForRepresentedObject(webFeed) configureCellsForRepresentedObject(webFeed)
} }
} }
@ -268,7 +268,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
renameAction.backgroundColor = UIColor.systemOrange renameAction.backgroundColor = UIColor.systemOrange
actions.append(renameAction) actions.append(renameAction)
if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed { if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed {
let moreTitle = NSLocalizedString("More", comment: "More") let moreTitle = NSLocalizedString("More", comment: "More")
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
@ -320,10 +320,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
} }
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else {
return nil return nil
} }
if feed is WebFeed { if feed is Feed {
return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true) return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
} else if feed is Folder { } else if feed is Folder {
return makeFolderContextMenu(indexPath: indexPath) return makeFolderContextMenu(indexPath: indexPath)
@ -795,7 +795,7 @@ private extension MasterFeedViewController {
cell.isDisclosureAvailable = false cell.isDisclosureAvailable = false
} }
if let feed = node.representedObject as? Feed { if let feed = node.representedObject as? SidebarItem {
cell.name = feed.nameForDisplay cell.name = feed.nameForDisplay
cell.unreadCount = feed.unreadCount cell.unreadCount = feed.unreadCount
} }
@ -812,7 +812,7 @@ private extension MasterFeedViewController {
} }
func configureIcon(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) { func configureIcon(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) {
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed, let feedID = feed.feedID else { guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? SidebarItem, let feedID = feed.sidebarItemID else {
return return
} }
cell.iconImage = IconImageCache.shared.imageFor(feedID) cell.iconImage = IconImageCache.shared.imageFor(feedID)
@ -832,9 +832,9 @@ private extension MasterFeedViewController {
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, IndexPath) -> Void) { func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, IndexPath) -> Void) {
applyToAvailableCells { (cell, indexPath) in applyToAvailableCells { (cell, indexPath) in
if let node = coordinator.nodeFor(indexPath), if let node = coordinator.nodeFor(indexPath),
let representedFeed = representedObject as? Feed, let representedFeed = representedObject as? SidebarItem,
let candidate = node.representedObject as? Feed, let candidate = node.representedObject as? SidebarItem,
representedFeed.feedID == candidate.feedID { representedFeed.sidebarItemID == candidate.sidebarItemID {
completion(cell, indexPath) completion(cell, indexPath)
} }
} }
@ -862,7 +862,7 @@ private extension MasterFeedViewController {
if let folder = node.representedObject as? Folder { if let folder = node.representedObject as? Folder {
return folder.account return folder.account
} }
if let feed = node.representedObject as? WebFeed { if let feed = node.representedObject as? Feed {
return feed.account return feed.account
} }
return nil return nil
@ -1000,7 +1000,7 @@ private extension MasterFeedViewController {
} }
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? { func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
let url = URL(string: webFeed.url) else { let url = URL(string: webFeed.url) else {
return nil return nil
} }
@ -1013,7 +1013,7 @@ private extension MasterFeedViewController {
} }
func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
let url = URL(string: webFeed.url) else { let url = URL(string: webFeed.url) else {
return nil return nil
} }
@ -1027,7 +1027,7 @@ private extension MasterFeedViewController {
} }
func copyHomePageAction(indexPath: IndexPath) -> UIAction? { func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
let homePageURL = webFeed.homePageURL, let homePageURL = webFeed.homePageURL,
let url = URL(string: homePageURL) else { let url = URL(string: homePageURL) else {
return nil return nil
@ -1041,7 +1041,7 @@ private extension MasterFeedViewController {
} }
func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
let homePageURL = webFeed.homePageURL, let homePageURL = webFeed.homePageURL,
let url = URL(string: homePageURL) else { let url = URL(string: homePageURL) else {
return nil return nil
@ -1056,7 +1056,7 @@ private extension MasterFeedViewController {
} }
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
webFeed.unreadCount > 0, webFeed.unreadCount > 0,
let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil return nil
@ -1096,7 +1096,7 @@ private extension MasterFeedViewController {
} }
func getInfoAction(indexPath: IndexPath) -> UIAction? { func getInfoAction(indexPath: IndexPath) -> UIAction? {
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else { guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
return nil return nil
} }
@ -1124,7 +1124,7 @@ private extension MasterFeedViewController {
} }
func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else { guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
return nil return nil
} }
@ -1137,7 +1137,7 @@ private extension MasterFeedViewController {
} }
func markAllAsReadAction(indexPath: IndexPath) -> UIAction? { func markAllAsReadAction(indexPath: IndexPath) -> UIAction? {
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed, guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem,
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView, let contentView = self.tableView.cellForRow(at: indexPath)?.contentView,
feed.unreadCount > 0 else { feed.unreadCount > 0 else {
return nil return nil
@ -1179,7 +1179,7 @@ private extension MasterFeedViewController {
func rename(indexPath: IndexPath) { func rename(indexPath: IndexPath) {
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return } guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return }
let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed") let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed")
let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String
@ -1196,7 +1196,7 @@ private extension MasterFeedViewController {
return return
} }
if let webFeed = feed as? WebFeed { if let webFeed = feed as? Feed {
webFeed.rename(to: name) { result in webFeed.rename(to: name) { result in
switch result { switch result {
case .success: case .success:
@ -1233,7 +1233,7 @@ private extension MasterFeedViewController {
} }
func delete(indexPath: IndexPath) { func delete(indexPath: IndexPath) {
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return } guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return }
let title: String let title: String
let message: String let message: String
@ -1271,7 +1271,7 @@ private extension MasterFeedViewController {
if let folder = deleteNode.representedObject as? Folder { if let folder = deleteNode.representedObject as? Folder {
ActivityManager.cleanUp(folder) ActivityManager.cleanUp(folder)
} else if let feed = deleteNode.representedObject as? WebFeed { } else if let feed = deleteNode.representedObject as? Feed {
ActivityManager.cleanUp(feed) ActivityManager.cleanUp(feed)
} }

View File

@ -453,7 +453,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
titleView.iconView.iconImage = coordinator.timelineIconImage titleView.iconView.iconImage = coordinator.timelineIconImage
} }
guard let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else { guard let feed = note.userInfo?[UserInfoKey.webFeed] as? Feed else {
return return
} }
tableView.indexPathsForVisibleRows?.forEach { indexPath in tableView.indexPathsForVisibleRows?.forEach { indexPath in
@ -631,7 +631,7 @@ private extension MasterTimelineViewController {
titleView.label.text = coordinator.timelineFeed?.nameForDisplay titleView.label.text = coordinator.timelineFeed?.nameForDisplay
updateTitleUnreadCount() updateTitleUnreadCount()
if coordinator.timelineFeed is WebFeed { if coordinator.timelineFeed is Feed {
titleView.buttonize() titleView.buttonize()
titleView.addGestureRecognizer(feedTapGestureRecognizer) titleView.addGestureRecognizer(feedTapGestureRecognizer)
} else { } else {

View File

@ -33,11 +33,11 @@ enum ShowFeedName {
struct FeedNode: Hashable { struct FeedNode: Hashable {
var node: Node var node: Node
var feedID: FeedIdentifier var feedID: SidebarItemIdentifier
init(_ node: Node) { init(_ node: Node) {
self.node = node self.node = node
self.feedID = (node.representedObject as! Feed).feedID! self.feedID = (node.representedObject as! SidebarItem).sidebarItemID!
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
@ -94,12 +94,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
private var lastExpandedTable = Set<ContainerIdentifier>() private var lastExpandedTable = Set<ContainerIdentifier>()
// Which Feeds have the Read Articles Filter enabled // Which Feeds have the Read Articles Filter enabled
private var readFilterEnabledTable = [FeedIdentifier: Bool]() private var readFilterEnabledTable = [SidebarItemIdentifier: Bool]()
// Flattened tree structure for the Sidebar // Flattened tree structure for the Sidebar
private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]() private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]()
private(set) var preSearchTimelineFeed: Feed? private(set) var preSearchTimelineFeed: SidebarItem?
private var lastSearchString = "" private var lastSearchString = ""
private var lastSearchScope: SearchScope? = nil private var lastSearchScope: SearchScope? = nil
private var isSearching: Bool = false private var isSearching: Bool = false
@ -157,7 +157,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
} }
var isReadArticlesFiltered: Bool { var isReadArticlesFiltered: Bool {
if let feedID = timelineFeed?.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] { if let feedID = timelineFeed?.sidebarItemID, let readFilterEnabled = readFilterEnabledTable[feedID] {
return readFilterEnabled return readFilterEnabled
} else { } else {
return timelineDefaultReadFilterType != .none return timelineDefaultReadFilterType != .none
@ -183,7 +183,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
} }
private var exceptionArticleFetcher: ArticleFetcher? private var exceptionArticleFetcher: ArticleFetcher?
private(set) var timelineFeed: Feed? private(set) var timelineFeed: SidebarItem?
var timelineMiddleIndexPath: IndexPath? var timelineMiddleIndexPath: IndexPath?
@ -363,7 +363,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
if let readArticlesFilterState = windowState[UserInfoKey.readArticlesFilterState] as? [[AnyHashable: AnyHashable]: Bool] { if let readArticlesFilterState = windowState[UserInfoKey.readArticlesFilterState] as? [[AnyHashable: AnyHashable]: Bool] {
for key in readArticlesFilterState.keys { for key in readArticlesFilterState.keys {
if let feedIdentifier = FeedIdentifier(userInfo: key) { if let feedIdentifier = SidebarItemIdentifier(userInfo: key) {
readFilterEnabledTable[feedIdentifier] = readArticlesFilterState[key] readFilterEnabledTable[feedIdentifier] = readArticlesFilterState[key]
} }
} }
@ -545,7 +545,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
} }
@objc func userDidAddFeed(_ notification: Notification) { @objc func userDidAddFeed(_ notification: Notification) {
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else { guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? Feed else {
return return
} }
discloseWebFeed(webFeed, animations: [.scroll, .navigation]) discloseWebFeed(webFeed, animations: [.scroll, .navigation])
@ -557,7 +557,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
} }
@objc func accountDidDownloadArticles(_ note: Notification) { @objc func accountDidDownloadArticles(_ note: Notification) {
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else { guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<Feed> else {
return return
} }
@ -625,7 +625,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
} }
func toggleReadArticlesFilter() { func toggleReadArticlesFilter() {
guard let feedID = timelineFeed?.feedID else { guard let feedID = timelineFeed?.sidebarItemID else {
return return
} }
@ -638,10 +638,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
refreshTimeline(resetScroll: false) refreshTimeline(resetScroll: false)
} }
func nodeFor(feedID: FeedIdentifier) -> Node? { func nodeFor(feedID: SidebarItemIdentifier) -> Node? {
return treeController.rootNode.descendantNode(where: { node in return treeController.rootNode.descendantNode(where: { node in
if let feed = node.representedObject as? Feed { if let feed = node.representedObject as? SidebarItem {
return feed.feedID == feedID return feed.sidebarItemID == feedID
} else { } else {
return false return false
} }
@ -783,7 +783,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
return indexPathFor(node) return indexPathFor(node)
} }
func selectFeed(_ feed: Feed?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { func selectFeed(_ feed: SidebarItem?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
let indexPath: IndexPath? = { let indexPath: IndexPath? = {
if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) { if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) {
return indexPath return indexPath
@ -807,7 +807,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
selectArticle(nil) selectArticle(nil)
} }
if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed { if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? SidebarItem {
self.activityManager.selecting(feed: feed) self.activityManager.selecting(feed: feed)
self.installTimelineControllerIfNecessary(animated: animations.contains(.navigation)) self.installTimelineControllerIfNecessary(animated: animations.contains(.navigation))
@ -1120,15 +1120,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred) markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred)
} }
func timelineFeedIsEqualTo(_ feed: WebFeed) -> Bool { func timelineFeedIsEqualTo(_ feed: Feed) -> Bool {
guard let timelineFeed = timelineFeed as? WebFeed else { guard let timelineFeed = timelineFeed as? Feed else {
return false return false
} }
return timelineFeed == feed return timelineFeed == feed
} }
func discloseWebFeed(_ webFeed: WebFeed, initialLoad: Bool = false, animations: Animations = [], completion: (() -> Void)? = nil) { func discloseWebFeed(_ webFeed: Feed, initialLoad: Bool = false, animations: Animations = [], completion: (() -> Void)? = nil) {
if isSearching { if isSearching {
masterTimelineViewController?.hideSearch() masterTimelineViewController?.hideSearch()
} }
@ -1145,10 +1145,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
markExpanded(parentFolder) markExpanded(parentFolder)
} }
if let webFeedFeedID = webFeed.feedID { if let webFeedFeedID = webFeed.sidebarItemID {
self.treeControllerDelegate.addFilterException(webFeedFeedID) self.treeControllerDelegate.addFilterException(webFeedFeedID)
} }
if let parentFolderFeedID = parentFolder?.feedID { if let parentFolderFeedID = parentFolder?.sidebarItemID {
self.treeControllerDelegate.addFilterException(parentFolderFeedID) self.treeControllerDelegate.addFilterException(parentFolderFeedID)
} }
@ -1196,7 +1196,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
} }
func showFeedInspector() { func showFeedInspector() {
let timelineWebFeed = timelineFeed as? WebFeed let timelineWebFeed = timelineFeed as? Feed
let articleFeed = currentArticle?.webFeed let articleFeed = currentArticle?.webFeed
guard let feed = timelineWebFeed ?? articleFeed else { guard let feed = timelineWebFeed ?? articleFeed else {
return return
@ -1204,7 +1204,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
showFeedInspector(for: feed) showFeedInspector(for: feed)
} }
func showFeedInspector(for feed: WebFeed) { func showFeedInspector(for feed: Feed) {
let feedInspectorNavController = let feedInspectorNavController =
UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController
let feedInspectorController = feedInspectorNavController.topViewController as! WebFeedInspectorViewController let feedInspectorController = feedInspectorNavController.topViewController as! WebFeedInspectorViewController
@ -1248,7 +1248,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
func homePageURLForFeed(_ indexPath: IndexPath) -> URL? { func homePageURLForFeed(_ indexPath: IndexPath) -> URL? {
guard let node = nodeFor(indexPath), guard let node = nodeFor(indexPath),
let feed = node.representedObject as? WebFeed, let feed = node.representedObject as? Feed,
let homePageURL = feed.homePageURL, let homePageURL = feed.homePageURL,
let url = URL(string: homePageURL) else { let url = URL(string: homePageURL) else {
return nil return nil
@ -1458,7 +1458,7 @@ private extension SceneCoordinator {
articleDictionaryNeedsUpdate = false articleDictionaryNeedsUpdate = false
} }
func ensureFeedIsAvailableToSelect(_ feed: Feed, completion: @escaping () -> Void) { func ensureFeedIsAvailableToSelect(_ feed: SidebarItem, completion: @escaping () -> Void) {
addToFilterExeptionsIfNecessary(feed) addToFilterExeptionsIfNecessary(feed)
addShadowTableToFilterExceptions() addShadowTableToFilterExceptions()
@ -1468,15 +1468,15 @@ private extension SceneCoordinator {
}) })
} }
func addToFilterExeptionsIfNecessary(_ feed: Feed?) { func addToFilterExeptionsIfNecessary(_ feed: SidebarItem?) {
if isReadFeedsFiltered, let feedID = feed?.feedID { if isReadFeedsFiltered, let feedID = feed?.sidebarItemID {
if feed is SmartFeed { if feed is SmartFeed {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} else if let folderFeed = feed as? Folder { } else if let folderFeed = feed as? Folder {
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil { if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} }
} else if let webFeed = feed as? WebFeed { } else if let webFeed = feed as? Feed {
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil { if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
addParentFolderToFilterExceptions(webFeed) addParentFolderToFilterExceptions(webFeed)
@ -1485,10 +1485,10 @@ private extension SceneCoordinator {
} }
} }
func addParentFolderToFilterExceptions(_ feed: Feed) { func addParentFolderToFilterExceptions(_ feed: SidebarItem) {
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject), guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
let folder = node.parent?.representedObject as? Folder, let folder = node.parent?.representedObject as? Folder,
let folderFeedID = folder.feedID else { let folderFeedID = folder.sidebarItemID else {
return return
} }
@ -1498,7 +1498,7 @@ private extension SceneCoordinator {
func addShadowTableToFilterExceptions() { func addShadowTableToFilterExceptions() {
for section in shadowTable { for section in shadowTable {
for feedNode in section.feedNodes { for feedNode in section.feedNodes {
if let feed = feedNode.node.representedObject as? Feed, let feedID = feed.feedID { if let feed = feedNode.node.representedObject as? SidebarItem, let feedID = feed.sidebarItemID {
treeControllerDelegate.addFilterException(feedID) treeControllerDelegate.addFilterException(feedID)
} }
} }
@ -1628,10 +1628,10 @@ private extension SceneCoordinator {
return ShadowTableChanges(deletes: deletes, inserts: inserts, moves: moves, rowChanges: changes) return ShadowTableChanges(deletes: deletes, inserts: inserts, moves: moves, rowChanges: changes)
} }
func shadowTableContains(_ feed: Feed) -> Bool { func shadowTableContains(_ feed: SidebarItem) -> Bool {
for section in shadowTable { for section in shadowTable {
for feedNode in section.feedNodes { for feedNode in section.feedNodes {
if let nodeFeed = feedNode.node.representedObject as? Feed, nodeFeed.feedID == feed.feedID { if let nodeFeed = feedNode.node.representedObject as? SidebarItem, nodeFeed.sidebarItemID == feed.sidebarItemID {
return true return true
} }
} }
@ -1652,7 +1652,7 @@ private extension SceneCoordinator {
return indexPathFor(node) return indexPathFor(node)
} }
func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) { func setTimelineFeed(_ feed: SidebarItem?, animated: Bool, completion: (() -> Void)? = nil) {
timelineFeed = feed timelineFeed = feed
fetchAndReplaceArticlesAsync(animated: animated) { fetchAndReplaceArticlesAsync(animated: animated) {
@ -1663,7 +1663,7 @@ private extension SceneCoordinator {
func updateShowNamesAndIcons() { func updateShowNamesAndIcons() {
if timelineFeed is WebFeed { if timelineFeed is Feed {
showFeedNames = { showFeedNames = {
for article in articles { for article in articles {
if !article.byline().isEmpty { if !article.byline().isEmpty {
@ -2074,11 +2074,11 @@ private extension SceneCoordinator {
return false return false
} }
func timelineFetcherContainsAnyFeed(_ feeds: Set<WebFeed>) -> Bool { func timelineFetcherContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
// Return true if theres a match or if a folder contains (recursively) one of feeds // Return true if theres a match or if a folder contains (recursively) one of feeds
if let feed = timelineFeed as? WebFeed { if let feed = timelineFeed as? Feed {
for oneFeed in feeds { for oneFeed in feeds {
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url { if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
return true return true
@ -2242,7 +2242,7 @@ private extension SceneCoordinator {
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
guard let userInfo = userInfo, guard let userInfo = userInfo,
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { let feedIdentifier = SidebarItemIdentifier(userInfo: feedIdentifierUserInfo) else {
return return
} }
@ -2327,7 +2327,7 @@ private extension SceneCoordinator {
func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool { func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo), let feedIdentifier = SidebarItemIdentifier(userInfo: feedIdentifierUserInfo),
let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool, let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool,
let articleWindowScrollY = userInfo[UserInfoKey.articleWindowScrollY] as? Int else { let articleWindowScrollY = userInfo[UserInfoKey.articleWindowScrollY] as? Int else {
return false return false
@ -2349,7 +2349,7 @@ private extension SceneCoordinator {
let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
if found { if found {
treeControllerDelegate.addFilterException(feedIdentifier) treeControllerDelegate.addFilterException(feedIdentifier)
if let webFeedNode = nodeFor(feedID: feedIdentifier), let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.feedID { if let webFeedNode = nodeFor(feedID: feedIdentifier), let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.sidebarItemID {
treeControllerDelegate.addFilterException(folderFeedID) treeControllerDelegate.addFilterException(folderFeedID)
} }
} }
@ -2379,13 +2379,13 @@ private extension SceneCoordinator {
} }
func findWebFeedNode(webFeedID: String, beginningAt startingNode: Node) -> Node? { func findWebFeedNode(webFeedID: String, beginningAt startingNode: Node) -> Node? {
if let node = startingNode.descendantNode(where: { ($0.representedObject as? WebFeed)?.webFeedID == webFeedID }) { if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.webFeedID == webFeedID }) {
return node return node
} }
return nil return nil
} }
func selectFeedAndArticle(feedIdentifier: FeedIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool { func selectFeedAndArticle(feedIdentifier: SidebarItemIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool {
guard let feedNode = nodeFor(feedID: feedIdentifier), let feedIndexPath = indexPathFor(feedNode) else { return false } guard let feedNode = nodeFor(feedID: feedIdentifier), let feedIndexPath = indexPathFor(feedNode) else { return false }
selectFeed(indexPath: feedIndexPath) { selectFeed(indexPath: feedIndexPath) {