mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Continue renaming webFeed to feed.
This commit is contained in:
@@ -57,7 +57,7 @@ public enum FetchType {
|
||||
case unread(_: Int? = nil)
|
||||
case today(_: Int? = nil)
|
||||
case folder(Folder, Bool)
|
||||
case webFeed(Feed)
|
||||
case feed(Feed)
|
||||
case articleIDs(Set<String>)
|
||||
case search(String)
|
||||
case searchWithArticleIDs(String, Set<String>)
|
||||
@@ -74,7 +74,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
public static let articleIDs = "articleIDs" // StatusesDidChange
|
||||
public static let statusKey = "statusKey" // StatusesDidChange
|
||||
public static let statusFlag = "statusFlag" // StatusesDidChange
|
||||
public static let webFeeds = "webFeeds" // AccountDidDownloadArticles, StatusesDidChange
|
||||
public static let feeds = "feeds" // AccountDidDownloadArticles, StatusesDidChange
|
||||
public static let syncErrors = "syncErrors" // AccountsDidFailToSyncWithErrors
|
||||
}
|
||||
|
||||
@@ -162,23 +162,23 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return nil
|
||||
}
|
||||
|
||||
private var webFeedDictionariesNeedUpdate = true
|
||||
private var _idToWebFeedDictionary = [String: Feed]()
|
||||
var idToWebFeedDictionary: [String: Feed] {
|
||||
if webFeedDictionariesNeedUpdate {
|
||||
rebuildWebFeedDictionaries()
|
||||
private var feedDictionariesNeedUpdate = true
|
||||
private var _idToFeedDictionary = [String: Feed]()
|
||||
var idToFeedDictionary: [String: Feed] {
|
||||
if feedDictionariesNeedUpdate {
|
||||
rebuildFeedDictionaries()
|
||||
}
|
||||
return _idToWebFeedDictionary
|
||||
return _idToFeedDictionary
|
||||
}
|
||||
private var _externalIDToWebFeedDictionary = [String: Feed]()
|
||||
var externalIDToWebFeedDictionary: [String: Feed] {
|
||||
if webFeedDictionariesNeedUpdate {
|
||||
rebuildWebFeedDictionaries()
|
||||
private var _externalIDToFeedDictionary = [String: Feed]()
|
||||
var externalIDToFeedDictionary: [String: Feed] {
|
||||
if feedDictionariesNeedUpdate {
|
||||
rebuildFeedDictionaries()
|
||||
}
|
||||
return _externalIDToWebFeedDictionary
|
||||
return _externalIDToFeedDictionary
|
||||
}
|
||||
|
||||
var flattenedWebFeedURLs: Set<String> {
|
||||
var flattenedFeedURLs: Set<String> {
|
||||
return Set(flattenedFeeds().map({ $0.url }))
|
||||
}
|
||||
|
||||
@@ -214,8 +214,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
private var unreadCounts = [String: Int]() // [feedID: Int]
|
||||
|
||||
private var _flattenedWebFeeds = Set<Feed>()
|
||||
private var flattenedWebFeedsNeedUpdate = true
|
||||
private var _flattenedFeeds = Set<Feed>()
|
||||
private var flattenedFeedsNeedUpdate = true
|
||||
|
||||
private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self)
|
||||
private lazy var metadataFile = AccountMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("Settings.plist"), account: self)
|
||||
@@ -225,9 +225,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var webFeedMetadataFile = WebFeedMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist"), account: self)
|
||||
typealias WebFeedMetadataDictionary = [String: WebFeedMetadata]
|
||||
var webFeedMetadata = WebFeedMetadataDictionary()
|
||||
private lazy var feedMetadataFile = FeedMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist"), account: self)
|
||||
typealias FeedMetadataDictionary = [String: FeedMetadata]
|
||||
var feedMetadata = FeedMetadataDictionary()
|
||||
|
||||
public var unreadCount = 0 {
|
||||
didSet {
|
||||
@@ -323,7 +323,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||
|
||||
metadataFile.load()
|
||||
webFeedMetadataFile.load()
|
||||
feedMetadataFile.load()
|
||||
opmlFile.load()
|
||||
|
||||
var shouldHandleRetentionPolicyChange = false
|
||||
@@ -339,7 +339,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
self.metadata.performedApril2020RetentionPolicyChange = true
|
||||
}
|
||||
|
||||
self.database.cleanupDatabaseAtStartup(subscribedToWebFeedIDs: self.flattenedFeeds().webFeedIDs())
|
||||
self.database.cleanupDatabaseAtStartup(subscribedToFeedIDs: self.flattenedFeeds().feedIDs())
|
||||
self.fetchAllUnreadCounts()
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
public func save() {
|
||||
metadataFile.save()
|
||||
webFeedMetadataFile.save()
|
||||
feedMetadataFile.save()
|
||||
opmlFile.save()
|
||||
}
|
||||
|
||||
@@ -506,13 +506,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
func addOPMLItems(_ items: [RSOPMLItem]) {
|
||||
for item in items {
|
||||
if let feedSpecifier = item.feedSpecifier {
|
||||
addWebFeed(newWebFeed(with: feedSpecifier))
|
||||
addFeed(newFeed(with: feedSpecifier))
|
||||
} else {
|
||||
if let title = item.titleFromAttributes, let folder = ensureFolder(with: title) {
|
||||
folder.externalID = item.attributes?["nnw_externalID"] as? String
|
||||
item.children?.forEach { itemChild in
|
||||
if let feedSpecifier = itemChild.feedSpecifier {
|
||||
folder.addWebFeed(newWebFeed(with: feedSpecifier))
|
||||
folder.addFeed(newFeed(with: feedSpecifier))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,13 +535,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return existingFolder(withExternalID: externalID)
|
||||
}
|
||||
|
||||
func existingContainers(withWebFeed webFeed: Feed) -> [Container] {
|
||||
func existingContainers(withFeed feed: Feed) -> [Container] {
|
||||
var containers = [Container]()
|
||||
if topLevelFeeds.contains(webFeed) {
|
||||
if topLevelFeeds.contains(feed) {
|
||||
containers.append(self)
|
||||
}
|
||||
folders?.forEach { folder in
|
||||
if folder.topLevelFeeds.contains(webFeed) {
|
||||
if folder.topLevelFeeds.contains(feed) {
|
||||
containers.append(folder)
|
||||
}
|
||||
}
|
||||
@@ -586,9 +586,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return folders?.first(where: { $0.externalID == externalID })
|
||||
}
|
||||
|
||||
func newWebFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
|
||||
func newFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
|
||||
let feedURL = opmlFeedSpecifier.feedURL
|
||||
let metadata = webFeedMetadata(feedURL: feedURL, feedID: feedURL)
|
||||
let metadata = feedMetadata(feedURL: feedURL, feedID: feedURL)
|
||||
let feed = Feed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata)
|
||||
if let feedTitle = opmlFeedSpecifier.title {
|
||||
if feed.name == nil {
|
||||
@@ -599,35 +599,35 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
|
||||
public func addFeed(_ feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.addWebFeed(for: self, with: feed, to: container, completion: completion)
|
||||
delegate.addFeed(for: self, with: feed, to: container, completion: completion)
|
||||
}
|
||||
|
||||
public func createFeed(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.createFeed(for: self, url: url, name: name, container: container, validateFeed: validateFeed, completion: completion)
|
||||
}
|
||||
|
||||
func createWebFeed(with name: String?, url: String, webFeedID: String, homePageURL: String?) -> Feed {
|
||||
let metadata = webFeedMetadata(feedURL: url, feedID: webFeedID)
|
||||
func createFeed(with name: String?, url: String, feedID: String, homePageURL: String?) -> Feed {
|
||||
let metadata = feedMetadata(feedURL: url, feedID: feedID)
|
||||
let feed = Feed(account: self, url: url, metadata: metadata)
|
||||
feed.name = name
|
||||
feed.homePageURL = homePageURL
|
||||
return feed
|
||||
}
|
||||
|
||||
public func removeWebFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeWebFeed(for: self, with: feed, from: container, completion: completion)
|
||||
public func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeFeed(for: self, with: feed, from: container, completion: completion)
|
||||
}
|
||||
|
||||
public func moveFeed(_ 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.moveFeed(for: self, with: feed, from: from, to: to, completion: completion)
|
||||
}
|
||||
|
||||
public func renameWebFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.renameWebFeed(for: self, with: feed, to: name, completion: completion)
|
||||
public func renameFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.renameFeed(for: self, with: feed, to: name, completion: completion)
|
||||
}
|
||||
|
||||
public func restoreWebFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.restoreWebFeed(for: self, feed: feed, container: container, completion: completion)
|
||||
public func restoreFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion)
|
||||
}
|
||||
|
||||
public func addFolder(_ name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
@@ -646,8 +646,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
||||
}
|
||||
|
||||
func clearWebFeedMetadata(_ feed: Feed) {
|
||||
webFeedMetadata[feed.url] = nil
|
||||
func clearFeedMetadata(_ feed: Feed) {
|
||||
feedMetadata[feed.url] = nil
|
||||
}
|
||||
|
||||
func addFolder(_ folder: Folder) {
|
||||
@@ -656,8 +656,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
structureDidChange()
|
||||
}
|
||||
|
||||
public func updateUnreadCounts(for webFeeds: Set<Feed>, completion: VoidCompletionBlock? = nil) {
|
||||
fetchUnreadCounts(for: webFeeds, completion: completion)
|
||||
public func updateUnreadCounts(for feeds: Set<Feed>, completion: VoidCompletionBlock? = nil) {
|
||||
fetchUnreadCounts(for: feeds, completion: completion)
|
||||
}
|
||||
|
||||
public func fetchArticles(_ fetchType: FetchType) throws -> Set<Article> {
|
||||
@@ -674,8 +674,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
} else {
|
||||
return try fetchArticles(folder: folder)
|
||||
}
|
||||
case .webFeed(let webFeed):
|
||||
return try fetchArticles(webFeed: webFeed)
|
||||
case .feed(let feed):
|
||||
return try fetchArticles(feed: feed)
|
||||
case .articleIDs(let articleIDs):
|
||||
return try fetchArticles(articleIDs: articleIDs)
|
||||
case .search(let searchString):
|
||||
@@ -699,8 +699,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
} else {
|
||||
return fetchArticlesAsync(folder: folder, completion)
|
||||
}
|
||||
case .webFeed(let webFeed):
|
||||
fetchArticlesAsync(webFeed: webFeed, completion)
|
||||
case .feed(let feed):
|
||||
fetchArticlesAsync(feed: feed, completion)
|
||||
case .articleIDs(let articleIDs):
|
||||
fetchArticlesAsync(articleIDs: articleIDs, completion)
|
||||
case .search(let searchString):
|
||||
@@ -711,15 +711,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
|
||||
public func fetchUnreadCountForToday(_ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
database.fetchUnreadCountForToday(for: flattenedFeeds().webFeedIDs(), completion: completion)
|
||||
database.fetchUnreadCountForToday(for: flattenedFeeds().feedIDs(), completion: completion)
|
||||
}
|
||||
|
||||
public func fetchUnreadCountForStarredArticles(_ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
database.fetchStarredAndUnreadCount(for: flattenedFeeds().webFeedIDs(), completion: completion)
|
||||
database.fetchStarredAndUnreadCount(for: flattenedFeeds().feedIDs(), completion: completion)
|
||||
}
|
||||
|
||||
public func fetchCountForStarredArticles() throws -> Int {
|
||||
return try database.fetchStarredArticlesCount(flattenedFeeds().webFeedIDs())
|
||||
return try database.fetchStarredArticlesCount(flattenedFeeds().feedIDs())
|
||||
}
|
||||
|
||||
public func fetchUnreadArticleIDs(_ completion: @escaping ArticleIDsCompletionBlock) {
|
||||
@@ -735,43 +735,43 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
|
||||
}
|
||||
|
||||
public func unreadCount(for webFeed: Feed) -> Int {
|
||||
return unreadCounts[webFeed.feedID] ?? 0
|
||||
public func unreadCount(for feed: Feed) -> Int {
|
||||
return unreadCounts[feed.feedID] ?? 0
|
||||
}
|
||||
|
||||
public func setUnreadCount(_ unreadCount: Int, for webFeed: Feed) {
|
||||
unreadCounts[webFeed.feedID] = unreadCount
|
||||
public func setUnreadCount(_ unreadCount: Int, for feed: Feed) {
|
||||
unreadCounts[feed.feedID] = unreadCount
|
||||
}
|
||||
|
||||
public func structureDidChange() {
|
||||
// Feeds were added or deleted. Or folders added or deleted.
|
||||
// Or feeds inside folders were added or deleted.
|
||||
opmlFile.markAsDirty()
|
||||
flattenedWebFeedsNeedUpdate = true
|
||||
webFeedDictionariesNeedUpdate = true
|
||||
flattenedFeedsNeedUpdate = true
|
||||
feedDictionariesNeedUpdate = true
|
||||
}
|
||||
|
||||
func update(_ webFeed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
// Used only by an On My Mac or iCloud account.
|
||||
precondition(Thread.isMainThread)
|
||||
precondition(type == .onMyMac || type == .cloudKit)
|
||||
|
||||
webFeed.takeSettings(from: parsedFeed)
|
||||
feed.takeSettings(from: parsedFeed)
|
||||
let parsedItems = parsedFeed.items
|
||||
guard !parsedItems.isEmpty else {
|
||||
completion(.success(ArticleChanges()))
|
||||
return
|
||||
}
|
||||
|
||||
update(webFeed.feedID, with: parsedItems, completion: completion)
|
||||
update(feed.feedID, with: parsedItems, completion: completion)
|
||||
}
|
||||
|
||||
func update(_ webFeedID: String, with parsedItems: Set<ParsedItem>, deleteOlder: Bool = true, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
func update(_ feedID: String, with parsedItems: Set<ParsedItem>, deleteOlder: Bool = true, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
// Used only by an On My Mac or iCloud account.
|
||||
precondition(Thread.isMainThread)
|
||||
precondition(type == .onMyMac || type == .cloudKit)
|
||||
|
||||
database.update(with: parsedItems, webFeedID: webFeedID, deleteOlder: deleteOlder) { updateArticlesResult in
|
||||
database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in
|
||||
switch updateArticlesResult {
|
||||
case .success(let articleChanges):
|
||||
self.sendNotificationAbout(articleChanges)
|
||||
@@ -782,16 +782,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
}
|
||||
|
||||
func update(webFeedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping DatabaseCompletionBlock) {
|
||||
func update(feedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping DatabaseCompletionBlock) {
|
||||
// Used only by syncing systems.
|
||||
precondition(Thread.isMainThread)
|
||||
precondition(type != .onMyMac && type != .cloudKit)
|
||||
guard !webFeedIDsAndItems.isEmpty else {
|
||||
guard !feedIDsAndItems.isEmpty else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
database.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
|
||||
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
|
||||
switch updateArticlesResult {
|
||||
case .success(let newAndUpdatedArticles):
|
||||
self.sendNotificationAbout(newAndUpdatedArticles)
|
||||
@@ -901,36 +901,36 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
public func flattenedFeeds() -> Set<Feed> {
|
||||
assert(Thread.isMainThread)
|
||||
if flattenedWebFeedsNeedUpdate {
|
||||
updateFlattenedWebFeeds()
|
||||
if flattenedFeedsNeedUpdate {
|
||||
updateFlattenedFeeds()
|
||||
}
|
||||
return _flattenedWebFeeds
|
||||
return _flattenedFeeds
|
||||
}
|
||||
|
||||
public func removeWebFeed(_ webFeed: Feed) {
|
||||
topLevelFeeds.remove(webFeed)
|
||||
public func removeFeed(_ feed: Feed) {
|
||||
topLevelFeeds.remove(feed)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func removeFeeds(_ webFeeds: Set<Feed>) {
|
||||
guard !webFeeds.isEmpty else {
|
||||
public func removeFeeds(_ feeds: Set<Feed>) {
|
||||
guard !feeds.isEmpty else {
|
||||
return
|
||||
}
|
||||
topLevelFeeds.subtract(webFeeds)
|
||||
topLevelFeeds.subtract(feeds)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func addWebFeed(_ webFeed: Feed) {
|
||||
topLevelFeeds.insert(webFeed)
|
||||
public func addFeed(_ feed: Feed) {
|
||||
topLevelFeeds.insert(feed)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
func addFeedIfNotInAnyFolder(_ webFeed: Feed) {
|
||||
if !flattenedFeeds().contains(webFeed) {
|
||||
addWebFeed(webFeed)
|
||||
func addFeedIfNotInAnyFolder(_ feed: Feed) {
|
||||
if !flattenedFeeds().contains(feed) {
|
||||
addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,8 +976,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
|
||||
@objc func batchUpdateDidPerform(_ note: Notification) {
|
||||
flattenedWebFeedsNeedUpdate = true
|
||||
rebuildWebFeedDictionaries()
|
||||
flattenedFeedsNeedUpdate = true
|
||||
rebuildFeedDictionaries()
|
||||
updateUnreadCount()
|
||||
}
|
||||
|
||||
@@ -1023,11 +1023,11 @@ extension Account: AccountMetadataDelegate {
|
||||
|
||||
// MARK: - FeedMetadataDelegate
|
||||
|
||||
extension Account: WebFeedMetadataDelegate {
|
||||
extension Account: FeedMetadataDelegate {
|
||||
|
||||
func valueDidChange(_ feedMetadata: WebFeedMetadata, key: WebFeedMetadata.CodingKeys) {
|
||||
webFeedMetadataFile.markAsDirty()
|
||||
guard let feed = existingWebFeed(withWebFeedID: feedMetadata.feedID) else {
|
||||
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
|
||||
feedMetadataFile.markAsDirty()
|
||||
guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else {
|
||||
return
|
||||
}
|
||||
feed.postFeedSettingDidChangeNotification(key)
|
||||
@@ -1039,11 +1039,11 @@ extension Account: WebFeedMetadataDelegate {
|
||||
private extension Account {
|
||||
|
||||
func fetchStarredArticles(limit: Int?) throws -> Set<Article> {
|
||||
return try database.fetchStarredArticles(flattenedFeeds().webFeedIDs(), limit)
|
||||
return try database.fetchStarredArticles(flattenedFeeds().feedIDs(), limit)
|
||||
}
|
||||
|
||||
func fetchStarredArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
database.fetchedStarredArticlesAsync(flattenedFeeds().webFeedIDs(), limit, completion)
|
||||
database.fetchedStarredArticlesAsync(flattenedFeeds().feedIDs(), limit, completion)
|
||||
}
|
||||
|
||||
func fetchUnreadArticles(limit: Int?) throws -> Set<Article> {
|
||||
@@ -1055,11 +1055,11 @@ private extension Account {
|
||||
}
|
||||
|
||||
func fetchTodayArticles(limit: Int?) throws -> Set<Article> {
|
||||
return try database.fetchTodayArticles(flattenedFeeds().webFeedIDs(), limit)
|
||||
return try database.fetchTodayArticles(flattenedFeeds().feedIDs(), limit)
|
||||
}
|
||||
|
||||
func fetchTodayArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
database.fetchTodayArticlesAsync(flattenedFeeds().webFeedIDs(), limit, completion)
|
||||
database.fetchTodayArticlesAsync(flattenedFeeds().feedIDs(), limit, completion)
|
||||
}
|
||||
|
||||
func fetchArticles(folder: Folder) throws -> Set<Article> {
|
||||
@@ -1078,17 +1078,17 @@ private extension Account {
|
||||
fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion)
|
||||
}
|
||||
|
||||
func fetchArticles(webFeed: Feed) throws -> Set<Article> {
|
||||
let articles = try database.fetchArticles(webFeed.feedID)
|
||||
validateUnreadCount(webFeed, articles)
|
||||
func fetchArticles(feed: Feed) throws -> Set<Article> {
|
||||
let articles = try database.fetchArticles(feed.feedID)
|
||||
validateUnreadCount(feed, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchArticlesAsync(webFeed: Feed, _ completion: @escaping ArticleSetResultBlock) {
|
||||
database.fetchArticlesAsync(webFeed.feedID) { [weak self] articleSetResult in
|
||||
func fetchArticlesAsync(feed: Feed, _ completion: @escaping ArticleSetResultBlock) {
|
||||
database.fetchArticlesAsync(feed.feedID) { [weak self] articleSetResult in
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
self?.validateUnreadCount(webFeed, articles)
|
||||
self?.validateUnreadCount(feed, articles)
|
||||
completion(.success(articles))
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
@@ -1097,7 +1097,7 @@ private extension Account {
|
||||
}
|
||||
|
||||
func fetchArticlesMatching(_ searchString: String) throws -> Set<Article> {
|
||||
return try database.fetchArticlesMatching(searchString, flattenedFeeds().webFeedIDs())
|
||||
return try database.fetchArticlesMatching(searchString, flattenedFeeds().feedIDs())
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) throws -> Set<Article> {
|
||||
@@ -1105,7 +1105,7 @@ private extension Account {
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingAsync(_ searchString: String, _ completion: @escaping ArticleSetResultBlock) {
|
||||
database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().webFeedIDs(), completion)
|
||||
database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), completion)
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
@@ -1120,25 +1120,25 @@ private extension Account {
|
||||
return database.fetchArticlesAsync(articleIDs: articleIDs, completion)
|
||||
}
|
||||
|
||||
func fetchUnreadArticles(webFeed: Feed) throws -> Set<Article> {
|
||||
let articles = try database.fetchUnreadArticles(Set([webFeed.feedID]), nil)
|
||||
validateUnreadCount(webFeed, articles)
|
||||
func fetchUnreadArticles(feed: Feed) throws -> Set<Article> {
|
||||
let articles = try database.fetchUnreadArticles(Set([feed.feedID]), nil)
|
||||
validateUnreadCount(feed, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchArticles(forContainer container: Container) throws -> Set<Article> {
|
||||
let feeds = container.flattenedFeeds()
|
||||
let articles = try database.fetchArticles(feeds.webFeedIDs())
|
||||
let articles = try database.fetchArticles(feeds.feedIDs())
|
||||
validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchArticlesAsync(forContainer container: Container, _ completion: @escaping ArticleSetResultBlock) {
|
||||
let webFeeds = container.flattenedFeeds()
|
||||
database.fetchArticlesAsync(webFeeds.webFeedIDs()) { [weak self] (articleSetResult) in
|
||||
let feeds = container.flattenedFeeds()
|
||||
database.fetchArticlesAsync(feeds.feedIDs()) { [weak self] (articleSetResult) in
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles)
|
||||
self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
|
||||
completion(.success(articles))
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
@@ -1148,8 +1148,8 @@ private extension Account {
|
||||
|
||||
func fetchUnreadArticles(forContainer container: Container, limit: Int?) throws -> Set<Article> {
|
||||
let feeds = container.flattenedFeeds()
|
||||
let articles = try database.fetchUnreadArticles(feeds.webFeedIDs(), limit)
|
||||
|
||||
let articles = try database.fetchUnreadArticles(feeds.feedIDs(), limit)
|
||||
|
||||
// We don't validate limit queries because they, by definition, won't correctly match the
|
||||
// complete unread state for the given container.
|
||||
if limit == nil {
|
||||
@@ -1160,15 +1160,15 @@ private extension Account {
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
let webFeeds = container.flattenedFeeds()
|
||||
database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs(), limit) { [weak self] (articleSetResult) in
|
||||
let feeds = container.flattenedFeeds()
|
||||
database.fetchUnreadArticlesAsync(feeds.feedIDs(), limit) { [weak self] (articleSetResult) in
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
|
||||
// We don't validate limit queries because they, by definition, won't correctly match the
|
||||
// complete unread state for the given container.
|
||||
if limit == nil {
|
||||
self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles)
|
||||
self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
|
||||
}
|
||||
|
||||
completion(.success(articles))
|
||||
@@ -1178,34 +1178,34 @@ private extension Account {
|
||||
}
|
||||
}
|
||||
|
||||
func validateUnreadCountsAfterFetchingUnreadArticles(_ webFeeds: Set<Feed>, _ articles: Set<Article>) {
|
||||
func validateUnreadCountsAfterFetchingUnreadArticles(_ feeds: Set<Feed>, _ articles: Set<Article>) {
|
||||
// Validate unread counts. This was the site of a performance slowdown:
|
||||
// it was calling going through the entire list of articles once per feed:
|
||||
// feeds.forEach { validateUnreadCount($0, articles) }
|
||||
// Now we loop through articles exactly once. This makes a huge difference.
|
||||
|
||||
var unreadCountStorage = [String: Int]() // [WebFeedID: Int]
|
||||
var unreadCountStorage = [String: Int]() // [FeedID: Int]
|
||||
for article in articles where !article.status.read {
|
||||
unreadCountStorage[article.webFeedID, default: 0] += 1
|
||||
unreadCountStorage[article.feedID, default: 0] += 1
|
||||
}
|
||||
webFeeds.forEach { (webFeed) in
|
||||
let unreadCount = unreadCountStorage[webFeed.feedID, default: 0]
|
||||
webFeed.unreadCount = unreadCount
|
||||
feeds.forEach { (feed) in
|
||||
let unreadCount = unreadCountStorage[feed.feedID, default: 0]
|
||||
feed.unreadCount = unreadCount
|
||||
}
|
||||
}
|
||||
|
||||
func validateUnreadCount(_ webFeed: Feed, _ articles: Set<Article>) {
|
||||
func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
|
||||
// articles must contain all the unread articles for the feed.
|
||||
// The unread number should match the feed’s unread count.
|
||||
|
||||
let feedUnreadCount = articles.reduce(0) { (result, article) -> Int in
|
||||
if article.webFeed == webFeed && !article.status.read {
|
||||
if article.feed == feed && !article.status.read {
|
||||
return result + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
webFeed.unreadCount = feedUnreadCount
|
||||
feed.unreadCount = feedUnreadCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,29 +1213,29 @@ private extension Account {
|
||||
|
||||
private extension Account {
|
||||
|
||||
func webFeedMetadata(feedURL: String, feedID: String) -> WebFeedMetadata {
|
||||
if let d = webFeedMetadata[feedURL] {
|
||||
func feedMetadata(feedURL: String, feedID: String) -> FeedMetadata {
|
||||
if let d = feedMetadata[feedURL] {
|
||||
assert(d.delegate === self)
|
||||
return d
|
||||
}
|
||||
let d = WebFeedMetadata(feedID: feedID)
|
||||
let d = FeedMetadata(feedID: feedID)
|
||||
d.delegate = self
|
||||
webFeedMetadata[feedURL] = d
|
||||
feedMetadata[feedURL] = d
|
||||
return d
|
||||
}
|
||||
|
||||
func updateFlattenedWebFeeds() {
|
||||
func updateFlattenedFeeds() {
|
||||
var feeds = Set<Feed>()
|
||||
feeds.formUnion(topLevelFeeds)
|
||||
for folder in folders! {
|
||||
feeds.formUnion(folder.flattenedFeeds())
|
||||
}
|
||||
|
||||
_flattenedWebFeeds = feeds
|
||||
flattenedWebFeedsNeedUpdate = false
|
||||
_flattenedFeeds = feeds
|
||||
flattenedFeedsNeedUpdate = false
|
||||
}
|
||||
|
||||
func rebuildWebFeedDictionaries() {
|
||||
func rebuildFeedDictionaries() {
|
||||
var idDictionary = [String: Feed]()
|
||||
var externalIDDictionary = [String: Feed]()
|
||||
|
||||
@@ -1246,9 +1246,9 @@ private extension Account {
|
||||
}
|
||||
}
|
||||
|
||||
_idToWebFeedDictionary = idDictionary
|
||||
_externalIDToWebFeedDictionary = externalIDDictionary
|
||||
webFeedDictionariesNeedUpdate = false
|
||||
_idToFeedDictionary = idDictionary
|
||||
_externalIDToFeedDictionary = externalIDDictionary
|
||||
feedDictionariesNeedUpdate = false
|
||||
}
|
||||
|
||||
func updateUnreadCount() {
|
||||
@@ -1263,7 +1263,7 @@ private extension Account {
|
||||
}
|
||||
|
||||
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
|
||||
let feeds = Set(articles.compactMap { $0.webFeed })
|
||||
let feeds = Set(articles.compactMap { $0.feed })
|
||||
let statuses = Set(articles.map { $0.status })
|
||||
let articleIDs = Set(articles.map { $0.articleID })
|
||||
|
||||
@@ -1271,7 +1271,7 @@ private extension Account {
|
||||
// which will update their own unread counts.
|
||||
updateUnreadCounts(for: feeds)
|
||||
|
||||
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.articleIDs: articleIDs, UserInfoKey.webFeeds: feeds])
|
||||
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.articleIDs: articleIDs, UserInfoKey.feeds: feeds])
|
||||
}
|
||||
|
||||
func noteStatusesForArticleIDsDidChange(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool) {
|
||||
@@ -1313,8 +1313,8 @@ private extension Account {
|
||||
}
|
||||
|
||||
func fetchUnreadCounts(_ feeds: Set<Feed>, _ completion: VoidCompletionBlock?) {
|
||||
let webFeedIDs = Set(feeds.map { $0.feedID })
|
||||
database.fetchUnreadCounts(for: webFeedIDs) { result in
|
||||
let feedIDs = Set(feeds.map { $0.feedID })
|
||||
database.fetchUnreadCounts(for: feedIDs) { result in
|
||||
if let unreadCountDictionary = try? result.get() {
|
||||
self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: feeds)
|
||||
}
|
||||
@@ -1351,13 +1351,13 @@ private extension Account {
|
||||
}
|
||||
|
||||
func sendNotificationAbout(_ articleChanges: ArticleChanges) {
|
||||
var webFeeds = Set<Feed>()
|
||||
var feeds = Set<Feed>()
|
||||
|
||||
if let newArticles = articleChanges.newArticles {
|
||||
webFeeds.formUnion(Set(newArticles.compactMap { $0.webFeed }))
|
||||
feeds.formUnion(Set(newArticles.compactMap { $0.feed }))
|
||||
}
|
||||
if let updatedArticles = articleChanges.updatedArticles {
|
||||
webFeeds.formUnion(Set(updatedArticles.compactMap { $0.webFeed }))
|
||||
feeds.formUnion(Set(updatedArticles.compactMap { $0.feed }))
|
||||
}
|
||||
|
||||
var shouldSendNotification = false
|
||||
@@ -1380,11 +1380,11 @@ private extension Account {
|
||||
}
|
||||
|
||||
if shouldUpdateUnreadCounts {
|
||||
self.updateUnreadCounts(for: webFeeds)
|
||||
self.updateUnreadCounts(for: feeds)
|
||||
}
|
||||
|
||||
if shouldSendNotification {
|
||||
userInfo[UserInfoKey.webFeeds] = webFeeds
|
||||
userInfo[UserInfoKey.feeds] = feeds
|
||||
NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
@@ -1394,12 +1394,12 @@ private extension Account {
|
||||
|
||||
extension Account {
|
||||
|
||||
public func existingWebFeed(withWebFeedID webFeedID: String) -> Feed? {
|
||||
return idToWebFeedDictionary[webFeedID]
|
||||
public func existingFeed(withFeedID feedID: String) -> Feed? {
|
||||
return idToFeedDictionary[feedID]
|
||||
}
|
||||
|
||||
public func existingWebFeed(withExternalID externalID: String) -> Feed? {
|
||||
return externalIDToWebFeedDictionary[externalID]
|
||||
public func existingFeed(withExternalID externalID: String) -> Feed? {
|
||||
return externalIDToFeedDictionary[externalID]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,13 +36,13 @@ protocol AccountDelegate {
|
||||
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 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: Feed, to name: String, 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: Feed, from container: 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 createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void)
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func restoreFeed(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 markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
@@ -203,9 +203,9 @@ public final class AccountManager: UnreadCountProvider {
|
||||
if let account = existingAccount(with: accountID) {
|
||||
return account.existingFolder(with: folderName)
|
||||
}
|
||||
case .webFeed(let accountID, let webFeedID):
|
||||
case .feed(let accountID, let feedID):
|
||||
if let account = existingAccount(with: accountID) {
|
||||
return account.existingWebFeed(withWebFeedID: webFeedID)
|
||||
return account.existingFeed(withFeedID: feedID)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -330,7 +330,7 @@ public final class AccountManager: UnreadCountProvider {
|
||||
|
||||
public func anyAccountHasAtLeastOneFeed() -> Bool {
|
||||
for account in activeAccounts {
|
||||
if account.hasAtLeastOneWebFeed() {
|
||||
if account.hasAtLeastOneFeed() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public protocol ArticleFetcher {
|
||||
extension Feed: ArticleFetcher {
|
||||
|
||||
public func fetchArticles() throws -> Set<Article> {
|
||||
return try account?.fetchArticles(.webFeed(self)) ?? Set<Article>()
|
||||
return try account?.fetchArticles(.feed(self)) ?? Set<Article>()
|
||||
}
|
||||
|
||||
public func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
|
||||
@@ -30,7 +30,7 @@ extension Feed: ArticleFetcher {
|
||||
completion(.success(Set<Article>()))
|
||||
return
|
||||
}
|
||||
account.fetchArticlesAsync(.webFeed(self), completion)
|
||||
account.fetchArticlesAsync(.feed(self), completion)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticles() throws -> Set<Article> {
|
||||
@@ -43,7 +43,7 @@ extension Feed: ArticleFetcher {
|
||||
completion(.success(Set<Article>()))
|
||||
return
|
||||
}
|
||||
account.fetchArticlesAsync(.webFeed(self)) { articleSetResult in
|
||||
account.fetchArticlesAsync(.feed(self)) { articleSetResult in
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
completion(.success(articles.unreadArticles()))
|
||||
|
||||
@@ -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<Feed, Error>) -> Void) {
|
||||
func createFeed(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 {
|
||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
||||
return
|
||||
@@ -183,13 +183,13 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
|
||||
let editedName = name == nil || name!.isEmpty ? nil : name
|
||||
|
||||
createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, validateFeed: validateFeed, completion: completion)
|
||||
createRSSFeed(for: account, url: url, editedName: editedName, container: container, validateFeed: validateFeed, completion: completion)
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let editedName = name.isEmpty ? nil : name
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
accountZone.renameWebFeed(feed, editedName: editedName) { result in
|
||||
accountZone.renameFeed(feed, editedName: editedName) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -202,19 +202,19 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
removeFeedFromCloud(for: account, with: feed, from: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
account.clearWebFeedMetadata(feed)
|
||||
container.removeWebFeed(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
container.removeFeed(feed)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case CloudKitZoneError.corruptAccount:
|
||||
// We got into a bad state and should remove the feed to clear up the bad data
|
||||
account.clearWebFeedMetadata(feed)
|
||||
container.removeWebFeed(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
container.removeFeed(feed)
|
||||
default:
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -222,14 +222,14 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: Feed, from fromContainer: Container, to toContainer: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func moveFeed(for account: Account, with feed: Feed, from fromContainer: Container, to toContainer: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
accountZone.moveWebFeed(feed, from: fromContainer, to: toContainer) { result in
|
||||
accountZone.moveFeed(feed, from: fromContainer, to: toContainer) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success:
|
||||
fromContainer.removeWebFeed(feed)
|
||||
toContainer.addWebFeed(feed)
|
||||
fromContainer.removeFeed(feed)
|
||||
toContainer.addFeed(feed)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
self.processAccountError(account, error)
|
||||
@@ -238,13 +238,13 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
accountZone.addWebFeed(feed, to: container) { result in
|
||||
accountZone.addFeed(feed, to: container) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success:
|
||||
container.addWebFeed(feed)
|
||||
container.addFeed(feed)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
self.processAccountError(account, error)
|
||||
@@ -253,8 +253,8 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
@@ -301,21 +301,21 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
accountZone.findWebFeedExternalIDs(for: folder) { result in
|
||||
accountZone.findFeedExternalIDs(for: folder) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success(let webFeedExternalIDs):
|
||||
case .success(let feedExternalIDs):
|
||||
|
||||
let webFeeds = webFeedExternalIDs.compactMap { account.existingWebFeed(withExternalID: $0) }
|
||||
let feeds = feedExternalIDs.compactMap { account.existingFeed(withExternalID: $0) }
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
for webFeed in webFeeds {
|
||||
for feed in feeds {
|
||||
group.enter()
|
||||
self.removeWebFeedFromCloud(for: account, with: webFeed, from: folder) { result in
|
||||
self.removeFeedFromCloud(for: account, with: feed, from: folder) { result in
|
||||
group.leave()
|
||||
if case .failure(let error) = result {
|
||||
os_log(.error, log: self.log, "Remove folder, remove webfeed error: %@.", error.localizedDescription)
|
||||
os_log(.error, log: self.log, "Remove folder, remove feed error: %@.", error.localizedDescription)
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
@@ -374,7 +374,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
folder.topLevelFeeds.remove(feed)
|
||||
|
||||
group.enter()
|
||||
self.restoreWebFeed(for: account, feed: feed, container: folder) { result in
|
||||
self.restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
group.leave()
|
||||
switch result {
|
||||
@@ -485,8 +485,8 @@ private extension CloudKitAccountDelegate {
|
||||
accountZone.fetchChangesInZone() { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
let webFeeds = account.flattenedFeeds()
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count)
|
||||
let feeds = account.flattenedFeeds()
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -495,7 +495,7 @@ private extension CloudKitAccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.combinedRefresh(account, webFeeds) { result in
|
||||
self.combinedRefresh(account, feeds) { result in
|
||||
self.refreshProgress.clear()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -518,8 +518,8 @@ private extension CloudKitAccountDelegate {
|
||||
|
||||
func standardRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
let intialWebFeedsCount = account.flattenedFeeds().count
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialWebFeedsCount)
|
||||
let intialFeedsCount = account.flattenedFeeds().count
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialFeedsCount)
|
||||
|
||||
func fail(_ error: Error) {
|
||||
self.processAccountError(account, error)
|
||||
@@ -532,14 +532,14 @@ private extension CloudKitAccountDelegate {
|
||||
case .success:
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
let webFeeds = account.flattenedFeeds()
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count - intialWebFeedsCount)
|
||||
let feeds = account.flattenedFeeds()
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count - intialFeedsCount)
|
||||
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshProgress.completeTask()
|
||||
self.combinedRefresh(account, webFeeds) { result in
|
||||
self.combinedRefresh(account, feeds) { result in
|
||||
self.sendArticleStatus(for: account, showProgress: true) { _ in
|
||||
self.refreshProgress.clear()
|
||||
if case .failure(let error) = result {
|
||||
@@ -562,12 +562,12 @@ private extension CloudKitAccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func combinedRefresh(_ account: Account, _ webFeeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func combinedRefresh(_ account: Account, _ feeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
refresher.refreshFeeds(webFeeds) {
|
||||
refresher.refreshFeeds(feeds) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
@@ -576,13 +576,13 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createRSSFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
func addDeadFeed() {
|
||||
let feed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
||||
container.addWebFeed(feed)
|
||||
let feed = account.createFeed(with: editedName, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil)
|
||||
container.addFeed(feed)
|
||||
|
||||
self.accountZone.createWebFeed(url: url.absoluteString,
|
||||
self.accountZone.createFeed(url: url.absoluteString,
|
||||
name: editedName,
|
||||
editedName: nil,
|
||||
homePageURL: nil,
|
||||
@@ -594,7 +594,7 @@ private extension CloudKitAccountDelegate {
|
||||
feed.externalID = externalID
|
||||
completion(.success(feed))
|
||||
case .failure(let error):
|
||||
container.removeWebFeed(feed)
|
||||
container.removeFeed(feed)
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
@@ -623,9 +623,9 @@ private extension CloudKitAccountDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
||||
let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil)
|
||||
feed.editedName = editedName
|
||||
container.addWebFeed(feed)
|
||||
container.addFeed(feed)
|
||||
|
||||
InitialFeedDownloader.download(url) { parsedFeed in
|
||||
self.refreshProgress.completeTask()
|
||||
@@ -635,7 +635,7 @@ private extension CloudKitAccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.accountZone.createWebFeed(url: bestFeedSpecifier.urlString,
|
||||
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
|
||||
name: parsedFeed.title,
|
||||
editedName: editedName,
|
||||
homePageURL: parsedFeed.homePageURL,
|
||||
@@ -648,7 +648,7 @@ private extension CloudKitAccountDelegate {
|
||||
self.sendNewArticlesToTheCloud(account, feed)
|
||||
completion(.success(feed))
|
||||
case .failure(let error):
|
||||
container.removeWebFeed(feed)
|
||||
container.removeFeed(feed)
|
||||
self.refreshProgress.completeTasks(2)
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -656,7 +656,7 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
container.removeWebFeed(feed)
|
||||
container.removeFeed(feed)
|
||||
self.refreshProgress.completeTasks(3)
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -664,7 +664,7 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
} else {
|
||||
self.refreshProgress.completeTasks(3)
|
||||
container.removeWebFeed(feed)
|
||||
container.removeFeed(feed)
|
||||
completion(.failure(AccountError.createErrorNotFound))
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
|
||||
func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
|
||||
account.fetchArticlesAsync(.webFeed(feed)) { result in
|
||||
account.fetchArticlesAsync(.feed(feed)) { result in
|
||||
switch result {
|
||||
case .success(let articles):
|
||||
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
|
||||
@@ -771,17 +771,17 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
|
||||
|
||||
func removeWebFeedFromCloud(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeFeedFromCloud(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
accountZone.removeWebFeed(feed, from: container) { result in
|
||||
accountZone.removeFeed(feed, from: container) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success:
|
||||
guard let webFeedExternalID = feed.externalID else {
|
||||
guard let feedExternalID = feed.externalID else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
self.articlesZone.deleteArticles(webFeedExternalID) { result in
|
||||
self.articlesZone.deleteArticles(feedExternalID) { result in
|
||||
feed.dropConditionalGetInfo()
|
||||
self.refreshProgress.completeTask()
|
||||
completion(result)
|
||||
|
||||
@@ -29,7 +29,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
weak var database: CKDatabase?
|
||||
var delegate: CloudKitZoneDelegate?
|
||||
|
||||
struct CloudKitWebFeed {
|
||||
struct CloudKitFeed {
|
||||
static let recordType = "AccountWebFeed"
|
||||
struct Fields {
|
||||
static let url = "url"
|
||||
@@ -60,13 +60,13 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
var feedRecords = [String: CKRecord]()
|
||||
|
||||
func processFeed(feedSpecifier: RSOPMLFeedSpecifier, containerExternalID: String) {
|
||||
if let webFeedRecord = feedRecords[feedSpecifier.feedURL], var containerExternalIDs = webFeedRecord[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
||||
if let feedRecord = feedRecords[feedSpecifier.feedURL], var containerExternalIDs = feedRecord[CloudKitFeed.Fields.containerExternalIDs] as? [String] {
|
||||
containerExternalIDs.append(containerExternalID)
|
||||
webFeedRecord[CloudKitWebFeed.Fields.containerExternalIDs] = containerExternalIDs
|
||||
feedRecord[CloudKitFeed.Fields.containerExternalIDs] = containerExternalIDs
|
||||
} else {
|
||||
let webFeedRecord = newWebFeedCKRecord(feedSpecifier: feedSpecifier, containerExternalID: containerExternalID)
|
||||
records.append(webFeedRecord)
|
||||
feedRecords[feedSpecifier.feedURL] = webFeedRecord
|
||||
let feedRecord = newFeedCKRecord(feedSpecifier: feedSpecifier, containerExternalID: containerExternalID)
|
||||
records.append(feedRecord)
|
||||
feedRecords[feedSpecifier.feedURL] = feedRecord
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,23 +90,23 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
|
||||
/// Persist a web feed record to iCloud and return the external key
|
||||
func createWebFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let recordID = CKRecord.ID(recordName: url.md5String, zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: recordID)
|
||||
record[CloudKitWebFeed.Fields.url] = url
|
||||
record[CloudKitWebFeed.Fields.name] = name
|
||||
let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID)
|
||||
record[CloudKitFeed.Fields.url] = url
|
||||
record[CloudKitFeed.Fields.name] = name
|
||||
if let editedName = editedName {
|
||||
record[CloudKitWebFeed.Fields.editedName] = editedName
|
||||
record[CloudKitFeed.Fields.editedName] = editedName
|
||||
}
|
||||
if let homePageURL = homePageURL {
|
||||
record[CloudKitWebFeed.Fields.homePageURL] = homePageURL
|
||||
record[CloudKitFeed.Fields.homePageURL] = homePageURL
|
||||
}
|
||||
|
||||
guard let containerExternalID = container.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
}
|
||||
record[CloudKitWebFeed.Fields.containerExternalIDs] = [containerExternalID]
|
||||
record[CloudKitFeed.Fields.containerExternalIDs] = [containerExternalID]
|
||||
|
||||
save(record) { result in
|
||||
switch result {
|
||||
@@ -119,15 +119,15 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
|
||||
/// Rename the given web feed
|
||||
func renameWebFeed(_ webFeed: Feed, editedName: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let externalID = webFeed.externalID else {
|
||||
func renameFeed(_ feed: Feed, editedName: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let externalID = feed.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
}
|
||||
|
||||
let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: recordID)
|
||||
record[CloudKitWebFeed.Fields.editedName] = editedName
|
||||
let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID)
|
||||
record[CloudKitFeed.Fields.editedName] = editedName
|
||||
|
||||
save(record) { result in
|
||||
switch result {
|
||||
@@ -140,22 +140,22 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
|
||||
/// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted
|
||||
func removeWebFeed(_ webFeed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
func removeFeed(_ feed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let fromContainerExternalID = from.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
}
|
||||
|
||||
fetch(externalID: webFeed.externalID) { result in
|
||||
fetch(externalID: feed.externalID) { result in
|
||||
switch result {
|
||||
case .success(let record):
|
||||
|
||||
if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
||||
if let containerExternalIDs = record[CloudKitFeed.Fields.containerExternalIDs] as? [String] {
|
||||
var containerExternalIDSet = Set(containerExternalIDs)
|
||||
containerExternalIDSet.remove(fromContainerExternalID)
|
||||
|
||||
if containerExternalIDSet.isEmpty {
|
||||
self.delete(externalID: webFeed.externalID) { result in
|
||||
self.delete(externalID: feed.externalID) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(true))
|
||||
@@ -166,7 +166,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
|
||||
} else {
|
||||
|
||||
record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
||||
record[CloudKitFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
||||
self.save(record) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -189,20 +189,20 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeed(_ webFeed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
}
|
||||
|
||||
fetch(externalID: webFeed.externalID) { result in
|
||||
fetch(externalID: feed.externalID) { result in
|
||||
switch result {
|
||||
case .success(let record):
|
||||
if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
||||
if let containerExternalIDs = record[CloudKitFeed.Fields.containerExternalIDs] as? [String] {
|
||||
var containerExternalIDSet = Set(containerExternalIDs)
|
||||
containerExternalIDSet.remove(fromContainerExternalID)
|
||||
containerExternalIDSet.insert(toContainerExternalID)
|
||||
record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
||||
record[CloudKitFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
||||
self.save(record, completion: completion)
|
||||
}
|
||||
case .failure(let error):
|
||||
@@ -211,19 +211,19 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(_ webFeed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func addFeed(_ feed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let toContainerExternalID = to.externalID else {
|
||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||
return
|
||||
}
|
||||
|
||||
fetch(externalID: webFeed.externalID) { result in
|
||||
fetch(externalID: feed.externalID) { result in
|
||||
switch result {
|
||||
case .success(let record):
|
||||
if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
||||
if let containerExternalIDs = record[CloudKitFeed.Fields.containerExternalIDs] as? [String] {
|
||||
var containerExternalIDSet = Set(containerExternalIDs)
|
||||
containerExternalIDSet.insert(toContainerExternalID)
|
||||
record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
||||
record[CloudKitFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
||||
self.save(record, completion: completion)
|
||||
}
|
||||
case .failure(let error):
|
||||
@@ -232,20 +232,20 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func findWebFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) {
|
||||
func findFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) {
|
||||
guard let folderExternalID = folder.externalID else {
|
||||
completion(.failure(CloudKitAccountZoneError.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
let predicate = NSPredicate(format: "containerExternalIDs CONTAINS %@", folderExternalID)
|
||||
let ckQuery = CKQuery(recordType: CloudKitWebFeed.recordType, predicate: predicate)
|
||||
let ckQuery = CKQuery(recordType: CloudKitFeed.recordType, predicate: predicate)
|
||||
|
||||
query(ckQuery) { result in
|
||||
switch result {
|
||||
case .success(let records):
|
||||
let webFeedExternalIds = records.map { $0.externalID }
|
||||
completion(.success(webFeedExternalIds))
|
||||
let feedExternalIds = records.map { $0.externalID }
|
||||
completion(.success(feedExternalIds))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -322,16 +322,16 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
|
||||
private extension CloudKitAccountZone {
|
||||
|
||||
func newWebFeedCKRecord(feedSpecifier: RSOPMLFeedSpecifier, containerExternalID: String) -> CKRecord {
|
||||
let record = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: generateRecordID())
|
||||
record[CloudKitWebFeed.Fields.url] = feedSpecifier.feedURL
|
||||
func newFeedCKRecord(feedSpecifier: RSOPMLFeedSpecifier, containerExternalID: String) -> CKRecord {
|
||||
let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: generateRecordID())
|
||||
record[CloudKitFeed.Fields.url] = feedSpecifier.feedURL
|
||||
if let editedName = feedSpecifier.title {
|
||||
record[CloudKitWebFeed.Fields.editedName] = editedName
|
||||
record[CloudKitFeed.Fields.editedName] = editedName
|
||||
}
|
||||
if let homePageURL = feedSpecifier.homePageURL {
|
||||
record[CloudKitWebFeed.Fields.homePageURL] = homePageURL
|
||||
record[CloudKitFeed.Fields.homePageURL] = homePageURL
|
||||
}
|
||||
record[CloudKitWebFeed.Fields.containerExternalIDs] = [containerExternalID]
|
||||
record[CloudKitFeed.Fields.containerExternalIDs] = [containerExternalID]
|
||||
return record
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ import Articles
|
||||
|
||||
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String)
|
||||
private var newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]()
|
||||
private var existingUnclaimedWebFeeds = [String: [Feed]]()
|
||||
private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String)
|
||||
private var newUnclaimedFeeds = [String: [UnclaimedFeed]]()
|
||||
private var existingUnclaimedFeeds = [String: [Feed]]()
|
||||
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||
|
||||
@@ -34,8 +34,8 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
for deletedRecordKey in deleted {
|
||||
switch deletedRecordKey.recordType {
|
||||
case CloudKitAccountZone.CloudKitWebFeed.recordType:
|
||||
removeWebFeed(deletedRecordKey.recordID.externalID)
|
||||
case CloudKitAccountZone.CloudKitFeed.recordType:
|
||||
removeFeed(deletedRecordKey.recordID.externalID)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
removeContainer(deletedRecordKey.recordID.externalID)
|
||||
default:
|
||||
@@ -45,8 +45,8 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
for changedRecord in changed {
|
||||
switch changedRecord.recordType {
|
||||
case CloudKitAccountZone.CloudKitWebFeed.recordType:
|
||||
addOrUpdateWebFeed(changedRecord)
|
||||
case CloudKitAccountZone.CloudKitFeed.recordType:
|
||||
addOrUpdateFeed(changedRecord)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
addOrUpdateContainer(changedRecord)
|
||||
default:
|
||||
@@ -57,36 +57,36 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func addOrUpdateWebFeed(_ record: CKRecord) {
|
||||
func addOrUpdateFeed(_ record: CKRecord) {
|
||||
guard let account = account,
|
||||
let urlString = record[CloudKitAccountZone.CloudKitWebFeed.Fields.url] as? String,
|
||||
let containerExternalIDs = record[CloudKitAccountZone.CloudKitWebFeed.Fields.containerExternalIDs] as? [String],
|
||||
let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String,
|
||||
let containerExternalIDs = record[CloudKitAccountZone.CloudKitFeed.Fields.containerExternalIDs] as? [String],
|
||||
let url = URL(string: urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
let name = record[CloudKitAccountZone.CloudKitWebFeed.Fields.name] as? String
|
||||
let editedName = record[CloudKitAccountZone.CloudKitWebFeed.Fields.editedName] as? String
|
||||
let homePageURL = record[CloudKitAccountZone.CloudKitWebFeed.Fields.homePageURL] as? String
|
||||
let name = record[CloudKitAccountZone.CloudKitFeed.Fields.name] as? String
|
||||
let editedName = record[CloudKitAccountZone.CloudKitFeed.Fields.editedName] as? String
|
||||
let homePageURL = record[CloudKitAccountZone.CloudKitFeed.Fields.homePageURL] as? String
|
||||
|
||||
if let webFeed = account.existingWebFeed(withExternalID: record.externalID) {
|
||||
updateWebFeed(webFeed, name: name, editedName: editedName, homePageURL: homePageURL, containerExternalIDs: containerExternalIDs)
|
||||
if let feed = account.existingFeed(withExternalID: record.externalID) {
|
||||
updateFeed(feed, name: name, editedName: editedName, homePageURL: homePageURL, containerExternalIDs: containerExternalIDs)
|
||||
} else {
|
||||
for containerExternalID in containerExternalIDs {
|
||||
if let container = account.existingContainer(withExternalID: containerExternalID) {
|
||||
createWebFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, container: container)
|
||||
createFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, feedExternalID: record.externalID, container: container)
|
||||
} else {
|
||||
addNewUnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, containerExternalID: containerExternalID)
|
||||
addNewUnclaimedFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, feedExternalID: record.externalID, containerExternalID: containerExternalID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeWebFeed(_ externalID: String) {
|
||||
if let webFeed = account?.existingWebFeed(withExternalID: externalID), let containers = account?.existingContainers(withWebFeed: webFeed) {
|
||||
func removeFeed(_ externalID: String) {
|
||||
if let feed = account?.existingFeed(withExternalID: externalID), let containers = account?.existingContainers(withFeed: feed) {
|
||||
containers.forEach {
|
||||
webFeed.dropConditionalGetInfo()
|
||||
$0.removeWebFeed(webFeed)
|
||||
feed.dropConditionalGetInfo()
|
||||
$0.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,24 +109,24 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
guard let container = folder, let containerExternalID = container.externalID else { return }
|
||||
|
||||
if let newUnclaimedWebFeeds = newUnclaimedWebFeeds[containerExternalID] {
|
||||
for newUnclaimedWebFeed in newUnclaimedWebFeeds {
|
||||
createWebFeedIfNecessary(url: newUnclaimedWebFeed.url,
|
||||
name: newUnclaimedWebFeed.name,
|
||||
editedName: newUnclaimedWebFeed.editedName,
|
||||
homePageURL: newUnclaimedWebFeed.homePageURL,
|
||||
webFeedExternalID: newUnclaimedWebFeed.webFeedExternalID,
|
||||
if let newUnclaimedFeeds = newUnclaimedFeeds[containerExternalID] {
|
||||
for newUnclaimedFeed in newUnclaimedFeeds {
|
||||
createFeedIfNecessary(url: newUnclaimedFeed.url,
|
||||
name: newUnclaimedFeed.name,
|
||||
editedName: newUnclaimedFeed.editedName,
|
||||
homePageURL: newUnclaimedFeed.homePageURL,
|
||||
feedExternalID: newUnclaimedFeed.feedExternalID,
|
||||
container: container)
|
||||
}
|
||||
|
||||
self.newUnclaimedWebFeeds.removeValue(forKey: containerExternalID)
|
||||
self.newUnclaimedFeeds.removeValue(forKey: containerExternalID)
|
||||
}
|
||||
|
||||
if let existingUnclaimedWebFeeds = existingUnclaimedWebFeeds[containerExternalID] {
|
||||
for existingUnclaimedWebFeed in existingUnclaimedWebFeeds {
|
||||
container.addWebFeed(existingUnclaimedWebFeed)
|
||||
if let existingUnclaimedFeeds = existingUnclaimedFeeds[containerExternalID] {
|
||||
for existingUnclaimedFeed in existingUnclaimedFeeds {
|
||||
container.addFeed(existingUnclaimedFeed)
|
||||
}
|
||||
self.existingUnclaimedWebFeeds.removeValue(forKey: containerExternalID)
|
||||
self.existingUnclaimedFeeds.removeValue(forKey: containerExternalID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,14 +140,14 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
private extension CloudKitAcountZoneDelegate {
|
||||
|
||||
func updateWebFeed(_ webFeed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
||||
func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
||||
guard let account = account else { return }
|
||||
|
||||
webFeed.name = name
|
||||
webFeed.editedName = editedName
|
||||
webFeed.homePageURL = homePageURL
|
||||
feed.name = name
|
||||
feed.editedName = editedName
|
||||
feed.homePageURL = homePageURL
|
||||
|
||||
let existingContainers = account.existingContainers(withWebFeed: webFeed)
|
||||
let existingContainers = account.existingContainers(withFeed: feed)
|
||||
let existingContainerExternalIds = existingContainers.compactMap { $0.externalID }
|
||||
|
||||
let diff = containerExternalIDs.difference(from: existingContainerExternalIds)
|
||||
@@ -156,50 +156,50 @@ private extension CloudKitAcountZoneDelegate {
|
||||
switch change {
|
||||
case .remove(_, let externalID, _):
|
||||
if let container = existingContainers.first(where: { $0.externalID == externalID }) {
|
||||
container.removeWebFeed(webFeed)
|
||||
container.removeFeed(feed)
|
||||
}
|
||||
case .insert(_, let externalID, _):
|
||||
if let container = account.existingContainer(withExternalID: externalID) {
|
||||
container.addWebFeed(webFeed)
|
||||
container.addFeed(feed)
|
||||
} else {
|
||||
addExistingUnclaimedWebFeed(webFeed, containerExternalID: externalID)
|
||||
addExistingUnclaimedFeed(feed, containerExternalID: externalID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createWebFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, container: Container) {
|
||||
func createFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, container: Container) {
|
||||
guard let account = account else { return }
|
||||
|
||||
if account.existingWebFeed(withExternalID: webFeedExternalID) != nil {
|
||||
if account.existingFeed(withExternalID: feedExternalID) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
let webFeed = account.createWebFeed(with: name, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: homePageURL)
|
||||
webFeed.editedName = editedName
|
||||
webFeed.externalID = webFeedExternalID
|
||||
container.addWebFeed(webFeed)
|
||||
let feed = account.createFeed(with: name, url: url.absoluteString, feedID: url.absoluteString, homePageURL: homePageURL)
|
||||
feed.editedName = editedName
|
||||
feed.externalID = feedExternalID
|
||||
container.addFeed(feed)
|
||||
}
|
||||
|
||||
func addNewUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) {
|
||||
if var unclaimedWebFeeds = self.newUnclaimedWebFeeds[containerExternalID] {
|
||||
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
|
||||
self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||
func addNewUnclaimedFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, containerExternalID: String) {
|
||||
if var unclaimedFeeds = self.newUnclaimedFeeds[containerExternalID] {
|
||||
unclaimedFeeds.append(UnclaimedFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, feedExternalID: feedExternalID))
|
||||
self.newUnclaimedFeeds[containerExternalID] = unclaimedFeeds
|
||||
} else {
|
||||
var unclaimedWebFeeds = [UnclaimedWebFeed]()
|
||||
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
|
||||
self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||
var unclaimedFeeds = [UnclaimedFeed]()
|
||||
unclaimedFeeds.append(UnclaimedFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, feedExternalID: feedExternalID))
|
||||
self.newUnclaimedFeeds[containerExternalID] = unclaimedFeeds
|
||||
}
|
||||
}
|
||||
|
||||
func addExistingUnclaimedWebFeed(_ webFeed: Feed, containerExternalID: String) {
|
||||
if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] {
|
||||
unclaimedWebFeeds.append(webFeed)
|
||||
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||
func addExistingUnclaimedFeed(_ feed: Feed, containerExternalID: String) {
|
||||
if var unclaimedFeeds = self.existingUnclaimedFeeds[containerExternalID] {
|
||||
unclaimedFeeds.append(feed)
|
||||
self.existingUnclaimedFeeds[containerExternalID] = unclaimedFeeds
|
||||
} else {
|
||||
var unclaimedWebFeeds = [Feed]()
|
||||
unclaimedWebFeeds.append(webFeed)
|
||||
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||
var unclaimedFeeds = [Feed]()
|
||||
unclaimedFeeds.append(feed)
|
||||
self.existingUnclaimedFeeds[containerExternalID] = unclaimedFeeds
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
static let recordType = "Article"
|
||||
struct Fields {
|
||||
static let articleStatus = "articleStatus"
|
||||
static let webFeedURL = "webFeedURL"
|
||||
static let feedURL = "webFeedURL"
|
||||
static let uniqueID = "uniqueID"
|
||||
static let title = "title"
|
||||
static let contentHTML = "contentHTML"
|
||||
@@ -51,7 +51,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
struct CloudKitArticleStatus {
|
||||
static let recordType = "ArticleStatus"
|
||||
struct Fields {
|
||||
static let webFeedExternalID = "webFeedExternalID"
|
||||
static let feedExternalID = "webFeedExternalID"
|
||||
static let read = "read"
|
||||
static let starred = "starred"
|
||||
}
|
||||
@@ -106,8 +106,8 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func deleteArticles(_ webFeedExternalID: String, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
let predicate = NSPredicate(format: "webFeedExternalID = %@", webFeedExternalID)
|
||||
func deleteArticles(_ feedExternalID: String, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
let predicate = NSPredicate(format: "webFeedExternalID = %@", feedExternalID)
|
||||
let ckQuery = CKQuery(recordType: CloudKitArticleStatus.recordType, predicate: predicate)
|
||||
delete(ckQuery: ckQuery, completion: completion)
|
||||
}
|
||||
@@ -190,8 +190,8 @@ private extension CloudKitArticlesZone {
|
||||
func makeStatusRecord(_ article: Article) -> CKRecord {
|
||||
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||
if let webFeedExternalID = article.webFeed?.externalID {
|
||||
record[CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID
|
||||
if let feedExternalID = article.feed?.externalID {
|
||||
record[CloudKitArticleStatus.Fields.feedExternalID] = feedExternalID
|
||||
}
|
||||
record[CloudKitArticleStatus.Fields.read] = article.status.read ? "1" : "0"
|
||||
record[CloudKitArticleStatus.Fields.starred] = article.status.starred ? "1" : "0"
|
||||
@@ -202,8 +202,8 @@ private extension CloudKitArticlesZone {
|
||||
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
|
||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||
|
||||
if let webFeedExternalID = statusUpdate.article?.webFeed?.externalID {
|
||||
record[CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID
|
||||
if let feedExternalID = statusUpdate.article?.feed?.externalID {
|
||||
record[CloudKitArticleStatus.Fields.feedExternalID] = feedExternalID
|
||||
}
|
||||
|
||||
record[CloudKitArticleStatus.Fields.read] = statusUpdate.isRead ? "1" : "0"
|
||||
@@ -218,7 +218,7 @@ private extension CloudKitArticlesZone {
|
||||
|
||||
let articleStatusRecordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
|
||||
record[CloudKitArticle.Fields.articleStatus] = CKRecord.Reference(recordID: articleStatusRecordID, action: .deleteSelf)
|
||||
record[CloudKitArticle.Fields.webFeedURL] = article.webFeed?.url
|
||||
record[CloudKitArticle.Fields.feedURL] = article.feed?.url
|
||||
record[CloudKitArticle.Fields.uniqueID] = article.uniqueID
|
||||
record[CloudKitArticle.Fields.title] = article.title
|
||||
record[CloudKitArticle.Fields.contentHTML] = article.contentHTML
|
||||
|
||||
@@ -137,12 +137,12 @@ private extension CloudKitArticlesZoneDelegate {
|
||||
group.enter()
|
||||
compressionQueue.async {
|
||||
let parsedItems = records.compactMap { self.makeParsedItem($0) }
|
||||
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
for (webFeedID, parsedItems) in webFeedIDsAndItems {
|
||||
for (feedID, parsedItems) in feedIDsAndItems {
|
||||
group.enter()
|
||||
self.account?.update(webFeedID, with: parsedItems, deleteOlder: false) { result in
|
||||
self.account?.update(feedID, with: parsedItems, deleteOlder: false) { result in
|
||||
switch result {
|
||||
case .success(let articleChanges):
|
||||
guard let deletes = articleChanges.deletedArticles, !deletes.isEmpty else {
|
||||
@@ -196,7 +196,7 @@ private extension CloudKitArticlesZoneDelegate {
|
||||
}
|
||||
|
||||
guard let uniqueID = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.uniqueID] as? String,
|
||||
let webFeedURL = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.webFeedURL] as? String else {
|
||||
let feedURL = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.feedURL] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ private extension CloudKitArticlesZoneDelegate {
|
||||
|
||||
let parsedItem = ParsedItem(syncServiceID: nil,
|
||||
uniqueID: uniqueID,
|
||||
feedURL: webFeedURL,
|
||||
feedURL: feedURL,
|
||||
url: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.url] as? String,
|
||||
externalURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.externalURL] as? String,
|
||||
title: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.title] as? String,
|
||||
|
||||
@@ -23,23 +23,23 @@ public protocol Container: AnyObject, ContainerIdentifiable {
|
||||
var folders: Set<Folder>? { get set }
|
||||
var externalID: String? { get set }
|
||||
|
||||
func hasAtLeastOneWebFeed() -> Bool
|
||||
func hasAtLeastOneFeed() -> Bool
|
||||
func objectIsChild(_ object: AnyObject) -> Bool
|
||||
|
||||
func hasChildFolder(with: String) -> Bool
|
||||
func childFolder(with: String) -> Folder?
|
||||
|
||||
func removeWebFeed(_ webFeed: Feed)
|
||||
func addWebFeed(_ webFeed: Feed)
|
||||
func removeFeed(_ feed: Feed)
|
||||
func addFeed(_ feed: Feed)
|
||||
|
||||
//Recursive — checks subfolders
|
||||
func flattenedFeeds() -> Set<Feed>
|
||||
func has(_ webFeed: Feed) -> Bool
|
||||
func hasWebFeed(with webFeedID: String) -> Bool
|
||||
func has(_ feed: Feed) -> Bool
|
||||
func hasFeed(with feedID: String) -> Bool
|
||||
func hasFeed(withURL url: String) -> Bool
|
||||
func existingWebFeed(withWebFeedID: String) -> Feed?
|
||||
func existingFeed(withFeedID: String) -> Feed?
|
||||
func existingFeed(withURL url: String) -> Feed?
|
||||
func existingWebFeed(withExternalID externalID: String) -> Feed?
|
||||
func existingFeed(withExternalID externalID: String) -> Feed?
|
||||
func existingFolder(with name: String) -> Folder?
|
||||
func existingFolder(withID: Int) -> Folder?
|
||||
|
||||
@@ -48,7 +48,7 @@ public protocol Container: AnyObject, ContainerIdentifiable {
|
||||
|
||||
public extension Container {
|
||||
|
||||
func hasAtLeastOneWebFeed() -> Bool {
|
||||
func hasAtLeastOneFeed() -> Bool {
|
||||
return topLevelFeeds.count > 0
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ public extension Container {
|
||||
return feeds
|
||||
}
|
||||
|
||||
func hasWebFeed(with webFeedID: String) -> Bool {
|
||||
return existingWebFeed(withWebFeedID: webFeedID) != nil
|
||||
func hasFeed(with feedID: String) -> Bool {
|
||||
return existingFeed(withFeedID: feedID) != nil
|
||||
}
|
||||
|
||||
func hasFeed(withURL url: String) -> Bool {
|
||||
@@ -101,9 +101,9 @@ public extension Container {
|
||||
return flattenedFeeds().contains(feed)
|
||||
}
|
||||
|
||||
func existingWebFeed(withWebFeedID webFeedID: String) -> Feed? {
|
||||
func existingFeed(withFeedID feedID: String) -> Feed? {
|
||||
for feed in flattenedFeeds() {
|
||||
if feed.feedID == webFeedID {
|
||||
if feed.feedID == feedID {
|
||||
return feed
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ public extension Container {
|
||||
return nil
|
||||
}
|
||||
|
||||
func existingWebFeed(withExternalID externalID: String) -> Feed? {
|
||||
func existingFeed(withExternalID externalID: String) -> Feed? {
|
||||
for feed in flattenedFeeds() {
|
||||
if feed.externalID == externalID {
|
||||
return feed
|
||||
|
||||
@@ -40,7 +40,7 @@ extension Feed {
|
||||
authors = Author.authorsWithParsedAuthors(parsedFeed.authors)
|
||||
}
|
||||
|
||||
func postFeedSettingDidChangeNotification(_ codingKey: WebFeedMetadata.CodingKeys) {
|
||||
func postFeedSettingDidChangeNotification(_ codingKey: FeedMetadata.CodingKeys) {
|
||||
let userInfo = [Feed.FeedSettingUserInfoKey: codingKey.stringValue]
|
||||
NotificationCenter.default.post(name: .FeedSettingDidChange, object: self, userInfo: userInfo)
|
||||
}
|
||||
@@ -56,8 +56,8 @@ public extension Article {
|
||||
return manager.existingAccount(with: accountID)
|
||||
}
|
||||
|
||||
var webFeed: Feed? {
|
||||
return account?.existingWebFeed(withWebFeedID: webFeedID)
|
||||
var feed: Feed? {
|
||||
return account?.existingFeed(withFeedID: feedID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// WebFeed.swift
|
||||
// Feed.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 7/1/17.
|
||||
@@ -22,7 +22,7 @@ public final class Feed: SidebarItem, Renamable, Hashable {
|
||||
assertionFailure("Expected feed.account, but got nil.")
|
||||
return nil
|
||||
}
|
||||
return SidebarItemIdentifier.webFeed(accountID, feedID)
|
||||
return SidebarItemIdentifier.feed(accountID, feedID)
|
||||
}
|
||||
|
||||
public weak var account: Account?
|
||||
@@ -203,7 +203,7 @@ public final class Feed: SidebarItem, Renamable, Hashable {
|
||||
|
||||
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)
|
||||
account.renameFeed(self, to: newName, completion: completion)
|
||||
}
|
||||
|
||||
// MARK: - UnreadCountProvider
|
||||
@@ -238,7 +238,7 @@ public final class Feed: SidebarItem, Renamable, Hashable {
|
||||
#endif
|
||||
}
|
||||
|
||||
var metadata: WebFeedMetadata
|
||||
var metadata: FeedMetadata
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@@ -246,7 +246,7 @@ public final class Feed: SidebarItem, Renamable, Hashable {
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(account: Account, url: String, metadata: WebFeedMetadata) {
|
||||
init(account: Account, url: String, metadata: FeedMetadata) {
|
||||
self.account = account
|
||||
self.accountID = account.accountID
|
||||
self.url = url
|
||||
@@ -306,16 +306,16 @@ extension Feed: OPMLRepresentable {
|
||||
|
||||
extension Set where Element == Feed {
|
||||
|
||||
func webFeedIDs() -> Set<String> {
|
||||
func feedIDs() -> Set<String> {
|
||||
return Set<String>(map { $0.feedID })
|
||||
}
|
||||
|
||||
func sorted() -> Array<Feed> {
|
||||
return sorted(by: { (webFeed1, webFeed2) -> Bool in
|
||||
if webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedSame {
|
||||
return webFeed1.url < webFeed2.url
|
||||
return sorted(by: { (feed1, feed2) -> Bool in
|
||||
if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame {
|
||||
return feed1.url < feed2.url
|
||||
}
|
||||
return webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedAscending
|
||||
return feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedAscending
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// WebFeedMetadata.swift
|
||||
// FeedMetadata.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 3/12/19.
|
||||
@@ -10,11 +10,11 @@ import Foundation
|
||||
import RSWeb
|
||||
import Articles
|
||||
|
||||
protocol WebFeedMetadataDelegate: AnyObject {
|
||||
func valueDidChange(_ feedMetadata: WebFeedMetadata, key: WebFeedMetadata.CodingKeys)
|
||||
protocol FeedMetadataDelegate: AnyObject {
|
||||
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys)
|
||||
}
|
||||
|
||||
final class WebFeedMetadata: Codable {
|
||||
final class FeedMetadata: Codable {
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case feedID
|
||||
@@ -137,7 +137,7 @@ final class WebFeedMetadata: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: WebFeedMetadataDelegate?
|
||||
weak var delegate: FeedMetadataDelegate?
|
||||
|
||||
init(feedID: String) {
|
||||
self.feedID = feedID
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// WebFeedMetadataFile.swift
|
||||
// FeedMetadataFile.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 9/13/19.
|
||||
@@ -10,9 +10,9 @@ import Foundation
|
||||
import os.log
|
||||
import RSCore
|
||||
|
||||
final class WebFeedMetadataFile {
|
||||
final class FeedMetadataFile {
|
||||
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "webFeedMetadataFile")
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "FeedMetadataFile")
|
||||
|
||||
private let fileURL: URL
|
||||
private let account: Account
|
||||
@@ -36,9 +36,9 @@ final class WebFeedMetadataFile {
|
||||
func load() {
|
||||
if let fileData = try? Data(contentsOf: fileURL) {
|
||||
let decoder = PropertyListDecoder()
|
||||
account.webFeedMetadata = (try? decoder.decode(Account.WebFeedMetadataDictionary.self, from: fileData)) ?? Account.WebFeedMetadataDictionary()
|
||||
account.feedMetadata = (try? decoder.decode(Account.FeedMetadataDictionary.self, from: fileData)) ?? Account.FeedMetadataDictionary()
|
||||
}
|
||||
account.webFeedMetadata.values.forEach { $0.delegate = account }
|
||||
account.feedMetadata.values.forEach { $0.delegate = account }
|
||||
}
|
||||
|
||||
func save() {
|
||||
@@ -59,7 +59,7 @@ final class WebFeedMetadataFile {
|
||||
|
||||
}
|
||||
|
||||
private extension WebFeedMetadataFile {
|
||||
private extension FeedMetadataFile {
|
||||
|
||||
func queueSaveToDiskIfNeeded() {
|
||||
saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
||||
@@ -72,10 +72,10 @@ private extension WebFeedMetadataFile {
|
||||
}
|
||||
}
|
||||
|
||||
private func metadataForOnlySubscribedToFeeds() -> Account.WebFeedMetadataDictionary {
|
||||
let webFeedIDs = account.idToWebFeedDictionary.keys
|
||||
return account.webFeedMetadata.filter { (feedID: String, metadata: WebFeedMetadata) -> Bool in
|
||||
return webFeedIDs.contains(metadata.feedID)
|
||||
private func metadataForOnlySubscribedToFeeds() -> Account.FeedMetadataDictionary {
|
||||
let feedIDs = account.idToFeedDictionary.keys
|
||||
return account.feedMetadata.filter { (feedID: String, metadata: FeedMetadata) -> Bool in
|
||||
return feedIDs.contains(metadata.feedID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
@@ -338,13 +338,13 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
|
||||
private func addFeedWranglerSubscription(account: Account, subscription sub: FeedWranglerSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
let feed = account.createWebFeed(with: sub.title, url: sub.feedURL, webFeedID: String(sub.feedID), homePageURL: sub.siteURL)
|
||||
let feed = account.createFeed(with: sub.title, url: sub.feedURL, feedID: String(sub.feedID), homePageURL: sub.siteURL)
|
||||
|
||||
account.addFeed(feed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
if let name = name {
|
||||
account.renameWebFeed(feed, to: name) { result in
|
||||
account.renameFeed(feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
@@ -383,7 +383,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
@@ -408,7 +408,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
// just add to account, folders are not supported
|
||||
DispatchQueue.main.async {
|
||||
account.addFeedIfNotInAnyFolder(feed)
|
||||
@@ -416,7 +416,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
@@ -427,8 +427,8 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.removeWebFeed(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
account.removeFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
@@ -442,11 +442,11 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
switch result {
|
||||
@@ -457,7 +457,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
@@ -540,7 +540,7 @@ private extension FeedWranglerAccountDelegate {
|
||||
subscriptions.forEach { subscription in
|
||||
let subscriptionId = String(subscription.feedID)
|
||||
|
||||
if let feed = account.existingWebFeed(withWebFeedID: subscriptionId) {
|
||||
if let feed = account.existingFeed(withFeedID: subscriptionId) {
|
||||
feed.name = subscription.title
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = subscription.siteURL
|
||||
@@ -552,9 +552,9 @@ private extension FeedWranglerAccountDelegate {
|
||||
|
||||
subscriptionsToAdd.forEach { subscription in
|
||||
let feedId = String(subscription.feedID)
|
||||
let feed = account.createWebFeed(with: subscription.title, url: subscription.feedURL, webFeedID: feedId, homePageURL: subscription.siteURL)
|
||||
let feed = account.createFeed(with: subscription.title, url: subscription.feedURL, feedID: feedId, homePageURL: subscription.siteURL)
|
||||
feed.externalID = nil
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,7 +568,7 @@ private extension FeedWranglerAccountDelegate {
|
||||
}
|
||||
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { $0.feedURL }).mapValues { Set($0) }
|
||||
account.update(webFeedIDsAndItems: feedIDsAndItems, defaultRead: true) { _ in
|
||||
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { _ in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ final class FeedbinAPICaller: NSObject {
|
||||
|
||||
}
|
||||
|
||||
func createTagging(webFeedID: Int, name: String, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
func createTagging(feedID: Int, name: String, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("taggings.json")
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
@@ -365,7 +365,7 @@ final class FeedbinAPICaller: NSObject {
|
||||
|
||||
let payload: Data
|
||||
do {
|
||||
payload = try JSONEncoder().encode(FeedbinCreateTagging(feedID: webFeedID, name: name))
|
||||
payload = try JSONEncoder().encode(FeedbinCreateTagging(feedID: feedID, name: name))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
|
||||
@@ -300,7 +300,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
guard folder.hasAtLeastOneWebFeed() else {
|
||||
guard folder.hasAtLeastOneFeed() else {
|
||||
folder.name = name
|
||||
return
|
||||
}
|
||||
@@ -328,7 +328,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// Feedbin uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system
|
||||
guard folder.hasAtLeastOneWebFeed() else {
|
||||
guard folder.hasAtLeastOneFeed() else {
|
||||
account.removeFolder(folder)
|
||||
completion(.success(()))
|
||||
return
|
||||
@@ -368,7 +368,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
}
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
|
||||
@@ -388,7 +388,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
caller.createSubscription(url: url) { result in
|
||||
@@ -420,7 +420,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.externalID else {
|
||||
@@ -447,7 +447,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if feed.folderRelationship?.count ?? 0 > 1 {
|
||||
deleteTagging(for: account, with: feed, from: container, completion: completion)
|
||||
} else {
|
||||
@@ -455,14 +455,14 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if from is Account {
|
||||
addWebFeed(for: account, with: feed, to: to, completion: completion)
|
||||
addFeed(for: account, with: feed, to: to, completion: completion)
|
||||
} else {
|
||||
deleteTagging(for: account, with: feed, from: from) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.addWebFeed(for: account, with: feed, to: to, completion: completion)
|
||||
self.addFeed(for: account, with: feed, to: to, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -470,18 +470,18 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func addFeed(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.feedID) {
|
||||
if let folder = container as? Folder, let feedID = Int(feed.feedID) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
caller.createTagging(webFeedID: webFeedID, name: folder.name ?? "") { result in
|
||||
caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success(let taggingID):
|
||||
DispatchQueue.main.async {
|
||||
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID))
|
||||
account.removeWebFeed(feed)
|
||||
folder.addWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
@@ -502,7 +502,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
@@ -514,7 +514,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
@@ -535,7 +535,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
folder.topLevelFeeds.remove(feed)
|
||||
|
||||
group.enter()
|
||||
restoreWebFeed(for: account, feed: feed, container: folder) { result in
|
||||
restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -781,7 +781,7 @@ private extension FeedbinAccountDelegate {
|
||||
folders.forEach { folder in
|
||||
if !tagNames.contains(folder.name ?? "") {
|
||||
for feed in folder.topLevelFeeds {
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
}
|
||||
account.removeFolder(folder)
|
||||
@@ -820,7 +820,7 @@ private extension FeedbinAccountDelegate {
|
||||
for folder in folders {
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !subFeedIds.contains(feed.feedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -828,7 +828,7 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
for feed in account.topLevelFeeds {
|
||||
if !subFeedIds.contains(feed.feedID) {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,7 +838,7 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
let subFeedId = String(subscription.feedID)
|
||||
|
||||
if let feed = account.existingWebFeed(withWebFeedID: subFeedId) {
|
||||
if let feed = account.existingFeed(withFeedID: subFeedId) {
|
||||
feed.name = subscription.name
|
||||
// If the name has been changed on the server remove the locally edited name
|
||||
feed.editedName = nil
|
||||
@@ -854,9 +854,9 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
// Actually add subscriptions all in one go, so we don’t trigger various rebuilding things that Account does.
|
||||
subscriptionsToAdd.forEach { subscription in
|
||||
let feed = account.createWebFeed(with: subscription.name, url: subscription.url, webFeedID: String(subscription.feedID), homePageURL: subscription.homePageURL)
|
||||
let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: String(subscription.feedID), homePageURL: subscription.homePageURL)
|
||||
feed.externalID = String(subscription.subscriptionID)
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,9 +890,9 @@ private extension FeedbinAccountDelegate {
|
||||
// Move any feeds not in the folder to the account
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !taggingFeedIDs.contains(feed.feedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,11 +902,11 @@ private extension FeedbinAccountDelegate {
|
||||
for tagging in groupedTaggings {
|
||||
let taggingFeedID = String(tagging.feedID)
|
||||
if !folderFeedIds.contains(taggingFeedID) {
|
||||
guard let feed = account.existingWebFeed(withWebFeedID: taggingFeedID) else {
|
||||
guard let feed = account.existingFeed(withFeedID: taggingFeedID) else {
|
||||
continue
|
||||
}
|
||||
saveFolderRelationship(for: feed, withFolderName: folderName, id: String(tagging.taggingID))
|
||||
folder.addWebFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,7 +917,7 @@ private extension FeedbinAccountDelegate {
|
||||
// Remove all feeds from the account container that have a tag
|
||||
for feed in account.topLevelFeeds {
|
||||
if taggedFeedIDs.contains(feed.feedID) {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1017,7 +1017,7 @@ private extension FeedbinAccountDelegate {
|
||||
}
|
||||
|
||||
if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) {
|
||||
createWebFeed(for: account, url: bestSpecifier.urlString, name: name, container: container, validateFeed: true, completion: completion)
|
||||
createFeed(for: account, url: bestSpecifier.urlString, name: name, container: container, validateFeed: true, completion: completion)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
@@ -1029,7 +1029,7 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
let feed = account.createWebFeed(with: sub.name, url: sub.url, webFeedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
feed.externalID = String(sub.subscriptionID)
|
||||
feed.iconURL = sub.jsonFeed?.icon
|
||||
feed.faviconURL = sub.jsonFeed?.favicon
|
||||
@@ -1038,7 +1038,7 @@ private extension FeedbinAccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
if let name = name {
|
||||
account.renameWebFeed(feed, to: name) { result in
|
||||
account.renameFeed(feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
@@ -1248,8 +1248,8 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
func processEntries(account: Account, entries: [FeedbinEntry]?, completion: @escaping DatabaseCompletionBlock) {
|
||||
let parsedItems = mapEntriesToParsedItems(entries: entries)
|
||||
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true, completion: completion)
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true, completion: completion)
|
||||
}
|
||||
|
||||
func mapEntriesToParsedItems(entries: [FeedbinEntry]?) -> Set<ParsedItem> {
|
||||
@@ -1381,7 +1381,7 @@ private extension FeedbinAccountDelegate {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
account.addFeedIfNotInAnyFolder(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
@@ -1394,7 +1394,7 @@ private extension FeedbinAccountDelegate {
|
||||
}
|
||||
} else {
|
||||
if let account = container as? Account {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
@@ -1415,11 +1415,11 @@ private extension FeedbinAccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.removeWebFeed(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
account.removeFeed(feed)
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
completion(.success(()))
|
||||
|
||||
@@ -314,7 +314,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
do {
|
||||
guard let credentials = credentials else {
|
||||
@@ -347,7 +347,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func renameFeed(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 }
|
||||
guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else {
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRenameFeed(feed.nameForDisplay, name)))
|
||||
@@ -374,7 +374,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
feed.editedName = name
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
do {
|
||||
guard let credentials = credentials else {
|
||||
@@ -405,7 +405,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeFeed(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 {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
|
||||
@@ -417,48 +417,48 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
folder.addWebFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func moveFeed(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 {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
|
||||
}
|
||||
}
|
||||
|
||||
addWebFeed(for: account, with: feed, to: to) { [weak self] addResult in
|
||||
addFeed(for: account, with: feed, to: to) { [weak self] addResult in
|
||||
switch addResult {
|
||||
// now that we have added the feed, remove it from the other collection
|
||||
case .success:
|
||||
self?.removeWebFeed(for: account, with: feed, from: from) { removeResult in
|
||||
self?.removeFeed(for: account, with: feed, from: from) { removeResult in
|
||||
switch removeResult {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure:
|
||||
from.addWebFeed(feed)
|
||||
from.addFeed(feed)
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to)))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
from.addWebFeed(feed)
|
||||
to.removeWebFeed(feed)
|
||||
from.addFeed(feed)
|
||||
to.removeFeed(feed)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// optimistically move the feed, undoing as appropriate to the failure
|
||||
from.removeWebFeed(feed)
|
||||
to.addWebFeed(feed)
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
switch result {
|
||||
@@ -469,7 +469,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
@@ -488,7 +488,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
folder.topLevelFeeds.remove(feed)
|
||||
|
||||
group.enter()
|
||||
restoreWebFeed(for: account, feed: feed, container: folder) { result in
|
||||
restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
@@ -19,7 +19,7 @@ struct FeedlyEntryParser {
|
||||
return entry.id
|
||||
}
|
||||
|
||||
/// When ingesting articles, the feedURL must match a feed's `webFeedID` for the article to be reachable between it and its matching feed. It reminds me of a foreign key.
|
||||
/// When ingesting articles, the feedURL must match a feed's `feedID` for the article to be reachable between it and its matching feed. It reminds me of a foreign key.
|
||||
var feedUrl: String? {
|
||||
guard let id = entry.origin?.streamId else {
|
||||
// At this point, check Feedly's API isn't glitching or the response has not changed structure.
|
||||
|
||||
@@ -17,7 +17,7 @@ struct FeedlyFeedParser {
|
||||
return rightToLeftTextSantizer.sanitize(feed.title) ?? ""
|
||||
}
|
||||
|
||||
var webFeedID: String {
|
||||
var feedID: String {
|
||||
return feed.id
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
|
||||
guard let handler = addCompletionHandler else {
|
||||
return
|
||||
}
|
||||
if let feedResource = feedResourceId, let feed = folder.existingWebFeed(withWebFeedID: feedResource.id) {
|
||||
if let feedResource = feedResourceId, let feed = folder.existingFeed(withFeedID: feedResource.id) {
|
||||
handler(.success(feed))
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -58,7 +58,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
.compactMap { (collectionFeed, folder) -> (Feed, Folder) in
|
||||
|
||||
// find an existing feed previously added to the account
|
||||
if let feed = account.existingWebFeed(withWebFeedID: collectionFeed.id) {
|
||||
if let feed = account.existingFeed(withFeedID: collectionFeed.id) {
|
||||
|
||||
// If the feed was renamed on Feedly, ensure we ingest the new name.
|
||||
if feed.nameForDisplay != collectionFeed.title {
|
||||
@@ -83,9 +83,9 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
|
||||
// no existing feed, create a new one
|
||||
let parser = FeedlyFeedParser(feed: collectionFeed)
|
||||
let feed = account.createWebFeed(with: parser.title,
|
||||
let feed = account.createFeed(with: parser.title,
|
||||
url: parser.url,
|
||||
webFeedID: parser.webFeedID,
|
||||
feedID: parser.feedID,
|
||||
homePageURL: parser.homePageURL)
|
||||
|
||||
// So the same feed isn't created more than once.
|
||||
@@ -97,7 +97,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
os_log(.debug, log: log, "Processing %i feeds.", feedsAndFolders.count)
|
||||
feedsAndFolders.forEach { (feed, folder) in
|
||||
if !folder.has(feed) {
|
||||
folder.addWebFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,15 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation {
|
||||
}
|
||||
|
||||
override func run() {
|
||||
let webFeedIDsAndItems = organisedItemsProvider.parsedItemsKeyedByFeedId
|
||||
let feedIDsAndItems = organisedItemsProvider.parsedItemsKeyedByFeedId
|
||||
|
||||
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true) { databaseError in
|
||||
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { databaseError in
|
||||
if let error = databaseError {
|
||||
self.didFinish(with: error)
|
||||
return
|
||||
}
|
||||
|
||||
os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", webFeedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName)
|
||||
os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", feedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName)
|
||||
self.didFinish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public final class Folder: SidebarItem, Renamable, Container, Hashable {
|
||||
return topLevelFeeds.contains(feed)
|
||||
}
|
||||
|
||||
public func addWebFeed(_ feed: Feed) {
|
||||
public func addFeed(_ feed: Feed) {
|
||||
topLevelFeeds.insert(feed)
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public final class Folder: SidebarItem, Renamable, Container, Hashable {
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func removeWebFeed(_ feed: Feed) {
|
||||
public func removeFeed(_ feed: Feed) {
|
||||
topLevelFeeds.remove(feed)
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
@@ -50,13 +50,13 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let webFeeds = account.flattenedFeeds()
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count)
|
||||
let feeds = account.flattenedFeeds()
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
refresher?.refreshFeeds(webFeeds) {
|
||||
refresher?.refreshFeeds(feeds) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
@@ -122,38 +122,38 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed(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 {
|
||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
createRSSWebFeed(for: account, url: url, editedName: name, container: container, completion: completion)
|
||||
createRSSFeed(for: account, url: url, editedName: name, container: container, completion: completion)
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
feed.editedName = name
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.removeWebFeed(feed)
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.removeFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
from.removeWebFeed(feed)
|
||||
to.addWebFeed(feed)
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.addWebFeed(feed)
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.addWebFeed(feed)
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ extension LocalAccountDelegate: LocalAccountRefresherDelegate {
|
||||
|
||||
private extension LocalAccountDelegate {
|
||||
|
||||
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createRSSFeed(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
|
||||
// container before the name has been downloaded. This will put it in the sidebar
|
||||
@@ -261,9 +261,9 @@ private extension LocalAccountDelegate {
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if let parsedFeed = parsedFeed {
|
||||
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
||||
let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil)
|
||||
feed.editedName = editedName
|
||||
container.addWebFeed(feed)
|
||||
container.addFeed(feed)
|
||||
|
||||
account.update(feed, with: parsedFeed, {_ in
|
||||
BatchUpdate.shared.end()
|
||||
|
||||
@@ -48,7 +48,7 @@ extension NewsBlurAccountDelegate {
|
||||
folders.forEach { folder in
|
||||
if !folderNames.contains(folder.name ?? "") {
|
||||
for feed in folder.topLevelFeeds {
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
}
|
||||
account.removeFolder(folder)
|
||||
@@ -86,7 +86,7 @@ extension NewsBlurAccountDelegate {
|
||||
for folder in folders {
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !newsBlurFeedIds.contains(feed.feedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ extension NewsBlurAccountDelegate {
|
||||
|
||||
for feed in account.topLevelFeeds {
|
||||
if !newsBlurFeedIds.contains(feed.feedID) {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,13 +103,13 @@ extension NewsBlurAccountDelegate {
|
||||
feeds.forEach { feed in
|
||||
let subFeedId = String(feed.feedID)
|
||||
|
||||
if let webFeed = account.existingWebFeed(withWebFeedID: subFeedId) {
|
||||
webFeed.name = feed.name
|
||||
if let feed = account.existingFeed(withFeedID: subFeedId) {
|
||||
feed.name = feed.name
|
||||
// If the name has been changed on the server remove the locally edited name
|
||||
webFeed.editedName = nil
|
||||
webFeed.homePageURL = feed.homePageURL
|
||||
webFeed.externalID = String(feed.feedID)
|
||||
webFeed.faviconURL = feed.faviconURL
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = feed.homePageURL
|
||||
feed.externalID = String(feed.feedID)
|
||||
feed.faviconURL = feed.faviconURL
|
||||
}
|
||||
else {
|
||||
feedsToAdd.insert(feed)
|
||||
@@ -118,9 +118,9 @@ extension NewsBlurAccountDelegate {
|
||||
|
||||
// Actually add feeds all in one go, so we don’t trigger various rebuilding things that Account does.
|
||||
feedsToAdd.forEach { feed in
|
||||
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||
webFeed.externalID = String(feed.feedID)
|
||||
account.addWebFeed(webFeed)
|
||||
let feed = account.createFeed(with: feed.name, url: feed.feedURL, feedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||
feed.externalID = String(feed.feedID)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +157,9 @@ extension NewsBlurAccountDelegate {
|
||||
// Move any feeds not in the folder to the account
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !newsBlurFolderFeedIDs.contains(feed.feedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +169,11 @@ extension NewsBlurAccountDelegate {
|
||||
for relationship in folderRelationships {
|
||||
let folderFeedID = String(relationship.feedID)
|
||||
if !folderFeedIds.contains(folderFeedID) {
|
||||
guard let feed = account.existingWebFeed(withWebFeedID: folderFeedID) else {
|
||||
guard let feed = account.existingFeed(withFeedID: folderFeedID) else {
|
||||
continue
|
||||
}
|
||||
saveFolderRelationship(for: feed, withFolderName: folderName, id: relationship.folderName)
|
||||
folder.addWebFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,12 +184,12 @@ extension NewsBlurAccountDelegate {
|
||||
let newsBlurFolderFeedIDs = folderRelationships.map { String($0.feedID) }
|
||||
for feed in account.topLevelFeeds {
|
||||
if !newsBlurFolderFeedIDs.contains(feed.feedID) {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for feed in account.topLevelFeeds {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,24 +419,24 @@ extension NewsBlurAccountDelegate {
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||
webFeed.externalID = String(feed.feedID)
|
||||
webFeed.faviconURL = feed.faviconURL
|
||||
let feed = account.createFeed(with: feed.name, url: feed.feedURL, feedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||
feed.externalID = String(feed.feedID)
|
||||
feed.faviconURL = feed.faviconURL
|
||||
|
||||
account.addFeed(webFeed, to: container) { result in
|
||||
account.addFeed(feed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
if let name = name {
|
||||
account.renameWebFeed(webFeed, to: name) { result in
|
||||
account.renameFeed(feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: webFeed, completion: completion)
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.initialFeedDownload(account: account, feed: webFeed, completion: completion)
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
@@ -532,17 +532,17 @@ extension NewsBlurAccountDelegate {
|
||||
let feedID = feed.feedID
|
||||
|
||||
if folderName == nil {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
|
||||
if let folders = account.folders {
|
||||
for folder in folders where folderName != nil && folder.name == folderName {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
if account.existingWebFeed(withWebFeedID: feedID) != nil {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
if account.existingFeed(withFeedID: feedID) != nil {
|
||||
account.clearFeedMetadata(feed)
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
|
||||
@@ -331,17 +331,17 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
return datePublished >= since
|
||||
}
|
||||
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL }).mapValues {
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL }).mapValues {
|
||||
Set($0)
|
||||
}
|
||||
|
||||
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true) { error in
|
||||
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
completion(.success(!webFeedIDsAndItems.isEmpty))
|
||||
completion(.success(!feedIDsAndItems.isEmpty))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +423,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> ()) {
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> ()) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
let folderName = (container as? Folder)?.name
|
||||
@@ -442,7 +442,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
@@ -469,11 +469,11 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let folder = container as? Folder else {
|
||||
DispatchQueue.main.async {
|
||||
if let account = container as? Account {
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
@@ -483,16 +483,16 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
let folderName = folder.name ?? ""
|
||||
saveFolderRelationship(for: feed, withFolderName: folderName, id: folderName)
|
||||
folder.addWebFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
deleteFeed(for: account, with: feed, from: container, completion: completion)
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
@@ -509,8 +509,8 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
from.removeWebFeed(feed)
|
||||
to.addWebFeed(feed)
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
@@ -519,7 +519,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
switch result {
|
||||
@@ -530,7 +530,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
@@ -562,7 +562,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
case .success(let folder):
|
||||
for feed in feedsToRestore {
|
||||
group.enter()
|
||||
self.restoreWebFeed(for: account, feed: feed, container: folder) { result in
|
||||
self.restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
@@ -358,7 +358,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
}
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
|
||||
@@ -390,7 +390,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
guard let url = URL(string: url) else {
|
||||
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
||||
return
|
||||
@@ -439,7 +439,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.externalID else {
|
||||
@@ -466,7 +466,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let subscriptionID = feed.externalID else {
|
||||
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
||||
return
|
||||
@@ -478,11 +478,11 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.removeWebFeed(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
account.removeFeed(feed)
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
completion(.success(()))
|
||||
@@ -496,9 +496,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if from is Account {
|
||||
addWebFeed(for: account, with: feed, to: to, completion: completion)
|
||||
addFeed(for: account, with: feed, to: to, completion: completion)
|
||||
} else {
|
||||
guard
|
||||
let subscriptionId = feed.externalID,
|
||||
@@ -514,8 +514,8 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success:
|
||||
from.removeWebFeed(feed)
|
||||
to.addWebFeed(feed)
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
@@ -524,7 +524,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func addFeed(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 {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
caller.createTagging(subscriptionID: feedExternalID, tagName: folder.name ?? "") { result in
|
||||
@@ -533,8 +533,8 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
self.saveFolderRelationship(for: feed, folderExternalID: folder.externalID, feedExternalID: feedExternalID)
|
||||
account.removeWebFeed(feed)
|
||||
folder.addWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
@@ -554,7 +554,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
@@ -566,7 +566,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
@@ -587,7 +587,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
folder.topLevelFeeds.remove(feed)
|
||||
|
||||
group.enter()
|
||||
restoreWebFeed(for: account, feed: feed, container: folder) { result in
|
||||
restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -720,7 +720,7 @@ private extension ReaderAPIAccountDelegate {
|
||||
folders.forEach { folder in
|
||||
if !readerFolderExternalIDs.contains(folder.externalID ?? "") {
|
||||
for feed in folder.topLevelFeeds {
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
clearFolderRelationship(for: feed, folderExternalID: folder.externalID)
|
||||
}
|
||||
account.removeFolder(folder)
|
||||
@@ -760,7 +760,7 @@ private extension ReaderAPIAccountDelegate {
|
||||
for folder in folders {
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !subFeedIds.contains(feed.feedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -768,22 +768,22 @@ private extension ReaderAPIAccountDelegate {
|
||||
|
||||
for feed in account.topLevelFeeds {
|
||||
if !subFeedIds.contains(feed.feedID) {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.removeWebFeed(feed)
|
||||
account.clearFeedMetadata(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
// Add any feeds we don't have and update any we do
|
||||
subscriptions.forEach { subscription in
|
||||
|
||||
if let feed = account.existingWebFeed(withWebFeedID: subscription.feedID) {
|
||||
if let feed = account.existingFeed(withFeedID: subscription.feedID) {
|
||||
feed.name = subscription.name
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = subscription.homePageURL
|
||||
} else {
|
||||
let feed = account.createWebFeed(with: subscription.name, url: subscription.url, webFeedID: subscription.feedID, homePageURL: subscription.homePageURL)
|
||||
let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subscription.feedID, homePageURL: subscription.homePageURL)
|
||||
feed.externalID = subscription.feedID
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -820,9 +820,9 @@ private extension ReaderAPIAccountDelegate {
|
||||
// Move any feeds not in the folder to the account
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !taggingFeedIDs.contains(feed.feedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
folder.removeFeed(feed)
|
||||
clearFolderRelationship(for: feed, folderExternalID: folder.externalID)
|
||||
account.addWebFeed(feed)
|
||||
account.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,11 +832,11 @@ private extension ReaderAPIAccountDelegate {
|
||||
for subscription in groupedTaggings {
|
||||
let taggingFeedID = subscription.feedID
|
||||
if !folderFeedIds.contains(taggingFeedID) {
|
||||
guard let feed = account.existingWebFeed(withWebFeedID: taggingFeedID) else {
|
||||
guard let feed = account.existingFeed(withFeedID: taggingFeedID) else {
|
||||
continue
|
||||
}
|
||||
saveFolderRelationship(for: feed, folderExternalID: folderExternalID, feedExternalID: subscription.feedID)
|
||||
folder.addWebFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,7 +847,7 @@ private extension ReaderAPIAccountDelegate {
|
||||
// Remove all feeds from the account container that have a tag
|
||||
for feed in account.topLevelFeeds {
|
||||
if taggedFeedIDs.contains(feed.feedID) {
|
||||
account.removeWebFeed(feed)
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -921,14 +921,14 @@ private extension ReaderAPIAccountDelegate {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
let feed = account.createWebFeed(with: sub.name, url: sub.url, webFeedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
feed.externalID = String(sub.feedID)
|
||||
|
||||
account.addFeed(feed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
if let name = name {
|
||||
self.renameWebFeed(for: account, with: feed, to: name) { result in
|
||||
self.renameFeed(for: account, with: feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
@@ -952,7 +952,7 @@ private extension ReaderAPIAccountDelegate {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(5)
|
||||
|
||||
// Download the initial articles
|
||||
self.caller.retrieveItemIDs(type: .allForFeed, webFeedID: feed.feedID) { result in
|
||||
self.caller.retrieveItemIDs(type: .allForFeed, feedID: feed.feedID) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success(let articleIDs):
|
||||
@@ -1032,8 +1032,8 @@ private extension ReaderAPIAccountDelegate {
|
||||
|
||||
func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping VoidCompletionBlock) {
|
||||
let parsedItems = mapEntriesToParsedItems(account: account, entries: entries)
|
||||
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true) { _ in
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { _ in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,7 +554,7 @@ final class ReaderAPICaller: NSObject {
|
||||
|
||||
}
|
||||
|
||||
func retrieveItemIDs(type: ItemIDType, webFeedID: String? = nil, completion: @escaping ((Result<[String], Error>) -> Void)) {
|
||||
func retrieveItemIDs(type: ItemIDType, feedID: String? = nil, completion: @escaping ((Result<[String], Error>) -> Void)) {
|
||||
guard let baseURL = apiBaseURL else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
@@ -579,13 +579,13 @@ final class ReaderAPICaller: NSObject {
|
||||
queryItems.append(URLQueryItem(name: "ot", value: String(Int(sinceTimeInterval))))
|
||||
queryItems.append(URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue))
|
||||
case .allForFeed:
|
||||
guard let webFeedID = webFeedID else {
|
||||
guard let feedID = feedID else {
|
||||
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
let sinceTimeInterval = (Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()).timeIntervalSince1970
|
||||
queryItems.append(URLQueryItem(name: "ot", value: String(Int(sinceTimeInterval))))
|
||||
queryItems.append(URLQueryItem(name: "s", value: webFeedID))
|
||||
queryItems.append(URLQueryItem(name: "s", value: feedID))
|
||||
case .unread:
|
||||
queryItems.append(URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue))
|
||||
queryItems.append(URLQueryItem(name: "xt", value: ReaderState.read.rawValue))
|
||||
|
||||
@@ -16,7 +16,7 @@ public enum SidebarItemIdentifier: CustomStringConvertible, Hashable, Equatable
|
||||
|
||||
case smartFeed(String) // String is a unique identifier
|
||||
case script(String) // String is a unique identifier
|
||||
case webFeed(String, String) // accountID, webFeedID
|
||||
case feed(String, String) // accountID, feedID
|
||||
case folder(String, String) // accountID, folderName
|
||||
|
||||
public var description: String {
|
||||
@@ -25,8 +25,8 @@ public enum SidebarItemIdentifier: CustomStringConvertible, Hashable, Equatable
|
||||
return "smartFeed: \(id)"
|
||||
case .script(let id):
|
||||
return "script: \(id)"
|
||||
case .webFeed(let accountID, let webFeedID):
|
||||
return "feed: \(accountID)_\(webFeedID)"
|
||||
case .feed(let accountID, let feedID):
|
||||
return "feed: \(accountID)_\(feedID)"
|
||||
case .folder(let accountID, let folderName):
|
||||
return "folder: \(accountID)_\(folderName)"
|
||||
}
|
||||
@@ -44,11 +44,11 @@ public enum SidebarItemIdentifier: CustomStringConvertible, Hashable, Equatable
|
||||
"type": "script",
|
||||
"id": id
|
||||
]
|
||||
case .webFeed(let accountID, let webFeedID):
|
||||
case .feed(let accountID, let feedID):
|
||||
return [
|
||||
"type": "feed",
|
||||
"accountID": accountID,
|
||||
"webFeedID": webFeedID
|
||||
"feedID": feedID
|
||||
]
|
||||
case .folder(let accountID, let folderName):
|
||||
return [
|
||||
@@ -70,8 +70,8 @@ public enum SidebarItemIdentifier: CustomStringConvertible, Hashable, Equatable
|
||||
guard let id = userInfo["id"] as? String else { return nil }
|
||||
self = SidebarItemIdentifier.script(id)
|
||||
case "feed":
|
||||
guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil }
|
||||
self = SidebarItemIdentifier.webFeed(accountID, webFeedID)
|
||||
guard let accountID = userInfo["accountID"] as? String, let feedID = userInfo["feedID"] as? String else { return nil }
|
||||
self = SidebarItemIdentifier.feed(accountID, feedID)
|
||||
case "folder":
|
||||
guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil }
|
||||
self = SidebarItemIdentifier.folder(accountID, folderName)
|
||||
|
||||
@@ -33,8 +33,8 @@ class AccountFeedbinFolderContentsSyncTest: XCTestCase {
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
let folder = account.folders?.filter { $0.name == "Developers" } .first!
|
||||
XCTAssertEqual(156, folder?.topLevelWebFeeds.count ?? 0)
|
||||
XCTAssertEqual(2, account.topLevelWebFeeds.count)
|
||||
XCTAssertEqual(156, folder?.topLevelFeeds.count ?? 0)
|
||||
XCTAssertEqual(2, account.topLevelFeeds.count)
|
||||
|
||||
// Test Adding a Feed to the folder
|
||||
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "JSON/taggings_add.json"
|
||||
@@ -45,8 +45,8 @@ class AccountFeedbinFolderContentsSyncTest: XCTestCase {
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(157, folder?.topLevelWebFeeds.count ?? 0)
|
||||
XCTAssertEqual(1, account.topLevelWebFeeds.count)
|
||||
XCTAssertEqual(157, folder?.topLevelFeeds.count ?? 0)
|
||||
XCTAssertEqual(1, account.topLevelFeeds.count)
|
||||
|
||||
// Test Deleting some Feeds from the folder
|
||||
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "JSON/taggings_delete.json"
|
||||
@@ -57,8 +57,8 @@ class AccountFeedbinFolderContentsSyncTest: XCTestCase {
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(153, folder?.topLevelWebFeeds.count ?? 0)
|
||||
XCTAssertEqual(5, account.topLevelWebFeeds.count)
|
||||
XCTAssertEqual(153, folder?.topLevelFeeds.count ?? 0)
|
||||
XCTAssertEqual(5, account.topLevelFeeds.count)
|
||||
|
||||
TestAccountManager.shared.deleteAccount(account)
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ class AccountFeedbinSyncTest: XCTestCase {
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(224, account.flattenedWebFeeds().count)
|
||||
XCTAssertEqual(224, account.flattenedFeeds().count)
|
||||
|
||||
let daringFireball = account.idToWebFeedDictionary["1296379"]
|
||||
let daringFireball = account.idToFeedDictionary["1296379"]
|
||||
XCTAssertEqual("Daring Fireball", daringFireball!.name)
|
||||
XCTAssertEqual("https://daringfireball.net/feeds/json", daringFireball!.url)
|
||||
XCTAssertEqual("https://daringfireball.net/", daringFireball!.homePageURL)
|
||||
@@ -57,9 +57,9 @@ class AccountFeedbinSyncTest: XCTestCase {
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(225, account.flattenedWebFeeds().count)
|
||||
XCTAssertEqual(225, account.flattenedFeeds().count)
|
||||
|
||||
let bPixels = account.idToWebFeedDictionary["1096623"]
|
||||
let bPixels = account.idToFeedDictionary["1096623"]
|
||||
XCTAssertEqual("Beautiful Pixels", bPixels?.name)
|
||||
XCTAssertEqual("https://feedpress.me/beautifulpixels", bPixels?.url)
|
||||
XCTAssertEqual("https://beautifulpixels.com/", bPixels?.homePageURL)
|
||||
|
||||
@@ -59,7 +59,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(account.flattenedWebFeeds().isEmpty, "Expected empty account.")
|
||||
XCTAssertTrue(account.flattenedFeeds().isEmpty, "Expected empty account.")
|
||||
|
||||
MainThreadOperationQueue.shared.add(createFeeds)
|
||||
|
||||
@@ -73,8 +73,8 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase {
|
||||
.flatMap { $0 }
|
||||
.map { $0.title })
|
||||
|
||||
let accountFeeds = account.flattenedWebFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.webFeedID })
|
||||
let accountFeeds = account.flattenedFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.feedID })
|
||||
let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay })
|
||||
|
||||
let missingIds = feedIds.subtracting(ingestedIds)
|
||||
@@ -92,7 +92,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase {
|
||||
let ingestedFolderAndFeedIds = (account.folders ?? Set())
|
||||
.sorted { $0.externalID! < $1.externalID! }
|
||||
.compactMap { folder -> [String: [String]]? in
|
||||
return [folder.externalID!: folder.topLevelWebFeeds.map { $0.webFeedID }.sorted(by: <)]
|
||||
return [folder.externalID!: folder.topLevelFeeds.map { $0.feedID }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds in their corresponding folders.")
|
||||
@@ -130,7 +130,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(account.flattenedWebFeeds().isEmpty, "Expected empty account.")
|
||||
XCTAssertTrue(account.flattenedFeeds().isEmpty, "Expected empty account.")
|
||||
|
||||
MainThreadOperationQueue.shared.add(createFeeds)
|
||||
|
||||
@@ -166,8 +166,8 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase {
|
||||
.flatMap { $0 }
|
||||
.map { $0.title })
|
||||
|
||||
let accountFeeds = account.flattenedWebFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.webFeedID })
|
||||
let accountFeeds = account.flattenedFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.feedID })
|
||||
let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay })
|
||||
|
||||
XCTAssertEqual(ingestedIds.count, feedIds.count)
|
||||
@@ -188,7 +188,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase {
|
||||
let ingestedFolderAndFeedIds = (account.folders ?? Set())
|
||||
.sorted { $0.externalID! < $1.externalID! }
|
||||
.compactMap { folder -> [String: [String]]? in
|
||||
return [folder.externalID!: folder.topLevelWebFeeds.map { $0.webFeedID }.sorted(by: <)]
|
||||
return [folder.externalID!: folder.topLevelFeeds.map { $0.feedID }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds to their corresponding folders.")
|
||||
|
||||
@@ -55,7 +55,7 @@ class FeedlyEntryParserTests: XCTestCase {
|
||||
XCTAssertEqual(item.uniqueID, entry.id)
|
||||
|
||||
// The following is not an error.
|
||||
// The feedURL must match the webFeedID for the article to be connected to its matching feed.
|
||||
// The feedURL must match the feedID for the article to be connected to its matching feed.
|
||||
XCTAssertEqual(item.feedURL, origin.streamId)
|
||||
XCTAssertEqual(item.title, entry.title)
|
||||
XCTAssertEqual(item.contentHTML, content.content)
|
||||
|
||||
@@ -22,7 +22,7 @@ class FeedlyFeedParserTests: XCTestCase {
|
||||
XCTAssertEqual(parser.title, name)
|
||||
XCTAssertEqual(parser.homePageURL, website)
|
||||
XCTAssertEqual(parser.url, url)
|
||||
XCTAssertEqual(parser.webFeedID, id)
|
||||
XCTAssertEqual(parser.feedID, id)
|
||||
}
|
||||
|
||||
func testSanitization() {
|
||||
@@ -36,6 +36,6 @@ class FeedlyFeedParserTests: XCTestCase {
|
||||
XCTAssertEqual(parser.title, name)
|
||||
XCTAssertEqual(parser.homePageURL, website)
|
||||
XCTAssertEqual(parser.url, url)
|
||||
XCTAssertEqual(parser.webFeedID, id)
|
||||
XCTAssertEqual(parser.feedID, id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ class FeedlyMirrorCollectionsAsFoldersOperationTests: XCTestCase {
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertFalse(account.flattenedWebFeeds().isEmpty, "Expected account to have feeds.")
|
||||
XCTAssertFalse(account.flattenedFeeds().isEmpty, "Expected account to have feeds.")
|
||||
}
|
||||
|
||||
// Now that the folders are added, remove them all.
|
||||
@@ -197,7 +197,7 @@ class FeedlyMirrorCollectionsAsFoldersOperationTests: XCTestCase {
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let feeds = account.flattenedWebFeeds()
|
||||
let feeds = account.flattenedFeeds()
|
||||
|
||||
XCTAssertTrue(feeds.isEmpty)
|
||||
}
|
||||
|
||||
@@ -134,12 +134,12 @@ class FeedlyTestSupport {
|
||||
return
|
||||
}
|
||||
let collectionFeeds = collection["feeds"] as! [[String: Any]]
|
||||
let folderFeeds = folder.topLevelWebFeeds
|
||||
let folderFeeds = folder.topLevelFeeds
|
||||
|
||||
XCTAssertEqual(collectionFeeds.count, folderFeeds.count)
|
||||
|
||||
let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String })
|
||||
let folderFeedIds = Set(folderFeeds.map { $0.webFeedID })
|
||||
let folderFeedIds = Set(folderFeeds.map { $0.feedID })
|
||||
let missingFeedIds = collectionFeedIds.subtracting(folderFeedIds)
|
||||
|
||||
XCTAssertTrue(missingFeedIds.isEmpty, "Feeds with these ids were not found in the \"\(label)\" folder.")
|
||||
@@ -210,7 +210,7 @@ class FeedlyTestSupport {
|
||||
for item in articleItems where item.id == article.articleID {
|
||||
XCTAssertEqual(article.uniqueID, item.id)
|
||||
XCTAssertEqual(article.contentHTML, item.content)
|
||||
XCTAssertEqual(article.webFeedID, item.feedId)
|
||||
XCTAssertEqual(article.feedID, item.feedId)
|
||||
XCTAssertEqual(article.externalURL, item.externalUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public struct Article: Hashable {
|
||||
|
||||
public let articleID: String // Unique database ID (possibly sync service ID)
|
||||
public let accountID: String
|
||||
public let webFeedID: String // Likely a URL, but not necessarily
|
||||
public let feedID: String // Likely a URL, but not necessarily
|
||||
public let uniqueID: String // Unique per feed (RSS guid, for example)
|
||||
public let title: String?
|
||||
public let contentHTML: String?
|
||||
@@ -28,9 +28,9 @@ public struct Article: Hashable {
|
||||
public let authors: Set<Author>?
|
||||
public let status: ArticleStatus
|
||||
|
||||
public init(accountID: String, articleID: String?, webFeedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, datePublished: Date?, dateModified: Date?, authors: Set<Author>?, status: ArticleStatus) {
|
||||
public init(accountID: String, articleID: String?, feedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, datePublished: Date?, dateModified: Date?, authors: Set<Author>?, status: ArticleStatus) {
|
||||
self.accountID = accountID
|
||||
self.webFeedID = webFeedID
|
||||
self.feedID = feedID
|
||||
self.uniqueID = uniqueID
|
||||
self.title = title
|
||||
self.contentHTML = contentHTML
|
||||
@@ -48,12 +48,12 @@ public struct Article: Hashable {
|
||||
self.articleID = articleID
|
||||
}
|
||||
else {
|
||||
self.articleID = Article.calculatedArticleID(webFeedID: webFeedID, uniqueID: uniqueID)
|
||||
self.articleID = Article.calculatedArticleID(feedID: feedID, uniqueID: uniqueID)
|
||||
}
|
||||
}
|
||||
|
||||
public static func calculatedArticleID(webFeedID: String, uniqueID: String) -> String {
|
||||
return databaseIDWithString("\(webFeedID) \(uniqueID)")
|
||||
public static func calculatedArticleID(feedID: String, uniqueID: String) -> String {
|
||||
return databaseIDWithString("\(feedID) \(uniqueID)")
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
@@ -65,7 +65,7 @@ public struct Article: Hashable {
|
||||
// MARK: - Equatable
|
||||
|
||||
static public func ==(lhs: Article, rhs: Article) -> Bool {
|
||||
return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.webFeedID == rhs.webFeedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.rawLink == rhs.rawLink && lhs.rawExternalLink == rhs.rawExternalLink && lhs.summary == rhs.summary && lhs.rawImageLink == rhs.rawImageLink && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors
|
||||
return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.feedID == rhs.feedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.rawLink == rhs.rawLink && lhs.rawExternalLink == rhs.rawExternalLink && lhs.summary == rhs.summary && lhs.rawImageLink == rhs.rawImageLink && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import Articles
|
||||
|
||||
// Main thread only.
|
||||
|
||||
public typealias UnreadCountDictionary = [String: Int] // webFeedID: unreadCount
|
||||
public typealias UnreadCountDictionary = [String: Int] // feedID: unreadCount
|
||||
public typealias UnreadCountDictionaryCompletionResult = Result<UnreadCountDictionary,DatabaseError>
|
||||
public typealias UnreadCountDictionaryCompletionBlock = (UnreadCountDictionaryCompletionResult) -> Void
|
||||
|
||||
@@ -90,36 +90,36 @@ public final class ArticlesDatabase {
|
||||
|
||||
// MARK: - Fetching Articles
|
||||
|
||||
public func fetchArticles(_ webFeedID: String) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticles(webFeedID)
|
||||
public func fetchArticles(_ feedID: String) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticles(feedID)
|
||||
}
|
||||
|
||||
public func fetchArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticles(webFeedIDs)
|
||||
public func fetchArticles(_ feedIDs: Set<String>) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticles(feedIDs)
|
||||
}
|
||||
|
||||
public func fetchArticles(articleIDs: Set<String>) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticles(articleIDs: articleIDs)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchUnreadArticles(webFeedIDs, limit)
|
||||
public func fetchUnreadArticles(_ feedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchUnreadArticles(feedIDs, limit)
|
||||
}
|
||||
|
||||
public func fetchTodayArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate(), limit)
|
||||
public func fetchTodayArticles(_ feedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticlesSince(feedIDs, todayCutoffDate(), limit)
|
||||
}
|
||||
|
||||
public func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchStarredArticles(webFeedIDs, limit)
|
||||
public func fetchStarredArticles(_ feedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchStarredArticles(feedIDs, limit)
|
||||
}
|
||||
|
||||
public func fetchStarredArticlesCount(_ webFeedIDs: Set<String>) throws -> Int {
|
||||
return try articlesTable.fetchStarredArticlesCount(webFeedIDs)
|
||||
public func fetchStarredArticlesCount(_ feedIDs: Set<String>) throws -> Int {
|
||||
return try articlesTable.fetchStarredArticlesCount(feedIDs)
|
||||
}
|
||||
|
||||
public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticlesMatching(searchString, webFeedIDs)
|
||||
public func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticlesMatching(searchString, feedIDs)
|
||||
}
|
||||
|
||||
public func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) throws -> Set<Article> {
|
||||
@@ -128,32 +128,32 @@ public final class ArticlesDatabase {
|
||||
|
||||
// MARK: - Fetching Articles Async
|
||||
|
||||
public func fetchArticlesAsync(_ webFeedID: String, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesAsync(webFeedID, completion)
|
||||
public func fetchArticlesAsync(_ feedID: String, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesAsync(feedID, completion)
|
||||
}
|
||||
|
||||
public func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesAsync(webFeedIDs, completion)
|
||||
public func fetchArticlesAsync(_ feedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesAsync(feedIDs, completion)
|
||||
}
|
||||
|
||||
public func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchUnreadArticlesAsync(webFeedIDs, limit, completion)
|
||||
public func fetchUnreadArticlesAsync(_ feedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchUnreadArticlesAsync(feedIDs, limit, completion)
|
||||
}
|
||||
|
||||
public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), limit, completion)
|
||||
public func fetchTodayArticlesAsync(_ feedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesSinceAsync(feedIDs, todayCutoffDate(), limit, completion)
|
||||
}
|
||||
|
||||
public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchStarredArticlesAsync(webFeedIDs, limit, completion)
|
||||
public func fetchedStarredArticlesAsync(_ feedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchStarredArticlesAsync(feedIDs, limit, completion)
|
||||
}
|
||||
|
||||
public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesMatchingAsync(searchString, webFeedIDs, completion)
|
||||
public func fetchArticlesMatchingAsync(_ searchString: String, _ feedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
articlesTable.fetchArticlesMatchingAsync(searchString, feedIDs, completion)
|
||||
}
|
||||
|
||||
public func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
@@ -174,8 +174,8 @@ public final class ArticlesDatabase {
|
||||
}
|
||||
|
||||
/// Fetch unread count for a single feed.
|
||||
public func fetchUnreadCount(_ webFeedID: String, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
let operation = FetchFeedUnreadCountOperation(webFeedID: webFeedID, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
|
||||
public func fetchUnreadCount(_ feedID: String, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
let operation = FetchFeedUnreadCountOperation(feedID: feedID, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
|
||||
operation.completionBlock = { operation in
|
||||
let fetchOperation = operation as! FetchFeedUnreadCountOperation
|
||||
completion(fetchOperation.result)
|
||||
@@ -183,9 +183,9 @@ public final class ArticlesDatabase {
|
||||
operationQueue.add(operation)
|
||||
}
|
||||
|
||||
/// Fetch non-zero unread counts for given webFeedIDs.
|
||||
public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
||||
let operation = FetchUnreadCountsForFeedsOperation(webFeedIDs: webFeedIDs, databaseQueue: queue)
|
||||
/// Fetch non-zero unread counts for given feedIDs.
|
||||
public func fetchUnreadCounts(for feedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
||||
let operation = FetchUnreadCountsForFeedsOperation(feedIDs: feedIDs, databaseQueue: queue)
|
||||
operation.completionBlock = { operation in
|
||||
let fetchOperation = operation as! FetchUnreadCountsForFeedsOperation
|
||||
completion(fetchOperation.result)
|
||||
@@ -193,30 +193,30 @@ public final class ArticlesDatabase {
|
||||
operationQueue.add(operation)
|
||||
}
|
||||
|
||||
public func fetchUnreadCountForToday(for webFeedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
fetchUnreadCount(for: webFeedIDs, since: todayCutoffDate(), completion: completion)
|
||||
public func fetchUnreadCountForToday(for feedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
fetchUnreadCount(for: feedIDs, since: todayCutoffDate(), completion: completion)
|
||||
}
|
||||
|
||||
public func fetchUnreadCount(for webFeedIDs: Set<String>, since: Date, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
articlesTable.fetchUnreadCount(webFeedIDs, since, completion)
|
||||
public func fetchUnreadCount(for feedIDs: Set<String>, since: Date, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
articlesTable.fetchUnreadCount(feedIDs, since, completion)
|
||||
}
|
||||
|
||||
public func fetchStarredAndUnreadCount(for webFeedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
articlesTable.fetchStarredAndUnreadCount(webFeedIDs, completion)
|
||||
public func fetchStarredAndUnreadCount(for feedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
articlesTable.fetchStarredAndUnreadCount(feedIDs, completion)
|
||||
}
|
||||
|
||||
// MARK: - Saving, Updating, and Deleting Articles
|
||||
|
||||
/// Update articles and save new ones — for feed-based systems (local and iCloud).
|
||||
public func update(with parsedItems: Set<ParsedItem>, webFeedID: String, deleteOlder: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
public func update(with parsedItems: Set<ParsedItem>, feedID: String, deleteOlder: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
precondition(retentionStyle == .feedBased)
|
||||
articlesTable.update(parsedItems, webFeedID, deleteOlder, completion)
|
||||
articlesTable.update(parsedItems, feedID, deleteOlder, completion)
|
||||
}
|
||||
|
||||
/// Update articles and save new ones — for sync systems (Feedbin, Feedly, etc.).
|
||||
public func update(webFeedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
public func update(feedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
precondition(retentionStyle == .syncSystem)
|
||||
articlesTable.update(webFeedIDsAndItems, defaultRead, completion)
|
||||
articlesTable.update(feedIDsAndItems, defaultRead, completion)
|
||||
}
|
||||
|
||||
/// Delete articles
|
||||
@@ -293,11 +293,11 @@ public final class ArticlesDatabase {
|
||||
/// This prevents the database from growing forever. If we didn’t do this:
|
||||
/// 1) The database would grow to an inordinate size, and
|
||||
/// 2) the app would become very slow.
|
||||
public func cleanupDatabaseAtStartup(subscribedToWebFeedIDs: Set<String>) {
|
||||
public func cleanupDatabaseAtStartup(subscribedToFeedIDs: Set<String>) {
|
||||
if retentionStyle == .syncSystem {
|
||||
articlesTable.deleteOldArticles()
|
||||
}
|
||||
articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToWebFeedIDs)
|
||||
articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToFeedIDs)
|
||||
articlesTable.deleteOldStatuses()
|
||||
}
|
||||
|
||||
|
||||
@@ -48,20 +48,20 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
// MARK: - Fetching Articles for Feed
|
||||
|
||||
func fetchArticles(_ webFeedID: String) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchArticlesForFeedID(webFeedID, $0) }
|
||||
func fetchArticles(_ feedID: String) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchArticlesForFeedID(feedID, $0) }
|
||||
}
|
||||
|
||||
func fetchArticlesAsync(_ webFeedID: String, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticlesForFeedID(webFeedID, $0) }, completion)
|
||||
func fetchArticlesAsync(_ feedID: String, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticlesForFeedID(feedID, $0) }, completion)
|
||||
}
|
||||
|
||||
func fetchArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchArticles(webFeedIDs, $0) }
|
||||
func fetchArticles(_ feedIDs: Set<String>) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchArticles(feedIDs, $0) }
|
||||
}
|
||||
|
||||
func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticles(webFeedIDs, $0) }, completion)
|
||||
func fetchArticlesAsync(_ feedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticles(feedIDs, $0) }, completion)
|
||||
}
|
||||
|
||||
// MARK: - Fetching Articles by articleID
|
||||
@@ -76,36 +76,36 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
// MARK: - Fetching Unread Articles
|
||||
|
||||
func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) }
|
||||
func fetchUnreadArticles(_ feedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchUnreadArticles(feedIDs, limit, $0) }
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, limit, $0) }, completion)
|
||||
func fetchUnreadArticlesAsync(_ feedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchUnreadArticles(feedIDs, limit, $0) }, completion)
|
||||
}
|
||||
|
||||
// MARK: - Fetching Today Articles
|
||||
|
||||
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }
|
||||
func fetchArticlesSince(_ feedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchArticlesSince(feedIDs, cutoffDate, limit, $0) }
|
||||
}
|
||||
|
||||
func fetchArticlesSinceAsync(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }, completion)
|
||||
func fetchArticlesSinceAsync(_ feedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticlesSince(feedIDs, cutoffDate, limit, $0) }, completion)
|
||||
}
|
||||
|
||||
// MARK: - Fetching Starred Articles
|
||||
|
||||
func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, limit, $0) }
|
||||
func fetchStarredArticles(_ feedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchStarredArticles(feedIDs, limit, $0) }
|
||||
}
|
||||
|
||||
func fetchStarredArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, limit, $0) }, completion)
|
||||
func fetchStarredArticlesAsync(_ feedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchStarredArticles(feedIDs, limit, $0) }, completion)
|
||||
}
|
||||
|
||||
func fetchStarredArticlesCount(_ webFeedIDs: Set<String>) throws -> Int {
|
||||
return try fetchArticlesCount{ self.fetchStarredArticlesCount(webFeedIDs, $0) }
|
||||
func fetchStarredArticlesCount(_ feedIDs: Set<String>) throws -> Int {
|
||||
return try fetchArticlesCount{ self.fetchStarredArticlesCount(feedIDs, $0) }
|
||||
}
|
||||
|
||||
// MARK: - Fetching Search Articles
|
||||
@@ -129,9 +129,9 @@ final class ArticlesTable: DatabaseTable {
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> {
|
||||
func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) throws -> Set<Article> {
|
||||
var articles = try fetchArticlesMatching(searchString)
|
||||
articles = articles.filter{ webFeedIDs.contains($0.webFeedID) }
|
||||
articles = articles.filter{ feedIDs.contains($0.feedID) }
|
||||
return articles
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ final class ArticlesTable: DatabaseTable {
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticlesMatching(searchString, webFeedIDs, $0) }, completion)
|
||||
func fetchArticlesMatchingAsync(_ searchString: String, _ feedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchArticlesMatching(searchString, feedIDs, $0) }, completion)
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
@@ -195,7 +195,7 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
// MARK: - Updating and Deleting
|
||||
|
||||
func update(_ parsedItems: Set<ParsedItem>, _ webFeedID: String, _ deleteOlder: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
func update(_ parsedItems: Set<ParsedItem>, _ feedID: String, _ deleteOlder: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
precondition(retentionStyle == .feedBased)
|
||||
if parsedItems.isEmpty {
|
||||
callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
@@ -220,13 +220,13 @@ final class ArticlesTable: DatabaseTable {
|
||||
let (statusesDictionary, _) = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
|
||||
assert(statusesDictionary.count == articleIDs.count)
|
||||
|
||||
let incomingArticles = Article.articlesWithParsedItems(parsedItems, webFeedID, self.accountID, statusesDictionary) //2
|
||||
let incomingArticles = Article.articlesWithParsedItems(parsedItems, feedID, self.accountID, statusesDictionary) //2
|
||||
if incomingArticles.isEmpty {
|
||||
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
}
|
||||
|
||||
let fetchedArticles = self.fetchArticlesForFeedID(webFeedID, database) //4
|
||||
let fetchedArticles = self.fetchArticlesForFeedID(feedID, database) //4
|
||||
let fetchedArticlesDictionary = fetchedArticles.dictionary()
|
||||
|
||||
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
||||
@@ -275,9 +275,9 @@ final class ArticlesTable: DatabaseTable {
|
||||
}
|
||||
}
|
||||
|
||||
func update(_ webFeedIDsAndItems: [String: Set<ParsedItem>], _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
func update(_ feedIDsAndItems: [String: Set<ParsedItem>], _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||
precondition(retentionStyle == .syncSystem)
|
||||
if webFeedIDsAndItems.isEmpty {
|
||||
if feedIDsAndItems.isEmpty {
|
||||
callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
}
|
||||
@@ -295,14 +295,14 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
var articleIDs = Set<String>()
|
||||
for (_, parsedItems) in webFeedIDsAndItems {
|
||||
for (_, parsedItems) in feedIDsAndItems {
|
||||
articleIDs.formUnion(parsedItems.articleIDs())
|
||||
}
|
||||
|
||||
let (statusesDictionary, _) = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
|
||||
assert(statusesDictionary.count == articleIDs.count)
|
||||
|
||||
let allIncomingArticles = Article.articlesWithWebFeedIDsAndItems(webFeedIDsAndItems, self.accountID, statusesDictionary) //2
|
||||
let allIncomingArticles = Article.articlesWithFeedIDsAndItems(feedIDsAndItems, self.accountID, statusesDictionary) //2
|
||||
if allIncomingArticles.isEmpty {
|
||||
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
|
||||
return
|
||||
@@ -371,9 +371,9 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
// MARK: - Unread Counts
|
||||
|
||||
func fetchUnreadCount(_ webFeedIDs: Set<String>, _ since: Date, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
func fetchUnreadCount(_ feedIDs: Set<String>, _ since: Date, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
// Get unread count for today, for instance.
|
||||
if webFeedIDs.isEmpty {
|
||||
if feedIDs.isEmpty {
|
||||
completion(.success(0))
|
||||
return
|
||||
}
|
||||
@@ -381,11 +381,11 @@ final class ArticlesTable: DatabaseTable {
|
||||
queue.runInDatabase { databaseResult in
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0;"
|
||||
|
||||
var parameters = [Any]()
|
||||
parameters += Array(webFeedIDs) as [Any]
|
||||
parameters += Array(feedIDs) as [Any]
|
||||
parameters += [since] as [Any]
|
||||
parameters += [since] as [Any]
|
||||
|
||||
@@ -407,8 +407,8 @@ final class ArticlesTable: DatabaseTable {
|
||||
}
|
||||
}
|
||||
|
||||
func fetchStarredAndUnreadCount(_ webFeedIDs: Set<String>, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
if webFeedIDs.isEmpty {
|
||||
func fetchStarredAndUnreadCount(_ feedIDs: Set<String>, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
if feedIDs.isEmpty {
|
||||
completion(.success(0))
|
||||
return
|
||||
}
|
||||
@@ -416,9 +416,9 @@ final class ArticlesTable: DatabaseTable {
|
||||
queue.runInDatabase { databaseResult in
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1;"
|
||||
let parameters = Array(webFeedIDs) as [Any]
|
||||
let parameters = Array(feedIDs) as [Any]
|
||||
|
||||
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
|
||||
|
||||
@@ -609,16 +609,16 @@ final class ArticlesTable: DatabaseTable {
|
||||
/// Delete articles from feeds that are no longer in the current set of subscribed-to feeds.
|
||||
/// This deletes from the articles and articleStatuses tables,
|
||||
/// and, via a trigger, it also deletes from the search index.
|
||||
func deleteArticlesNotInSubscribedToFeedIDs(_ webFeedIDs: Set<String>) {
|
||||
if webFeedIDs.isEmpty {
|
||||
func deleteArticlesNotInSubscribedToFeedIDs(_ feedIDs: Set<String>) {
|
||||
if feedIDs.isEmpty {
|
||||
return
|
||||
}
|
||||
queue.runInDatabase { databaseResult in
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
let sql = "select articleID from articles where feedID not in \(placeholders);"
|
||||
let parameters = Array(webFeedIDs) as [Any]
|
||||
let parameters = Array(feedIDs) as [Any]
|
||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
|
||||
return
|
||||
}
|
||||
@@ -820,24 +820,24 @@ private extension ArticlesTable {
|
||||
return articlesWithResultSet(resultSet, database)
|
||||
}
|
||||
|
||||
func fetchArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||
func fetchArticles(_ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
|
||||
if webFeedIDs.isEmpty {
|
||||
if feedIDs.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let parameters = feedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
let whereClause = "feedID in \(placeholders)"
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
|
||||
func fetchUnreadArticles(_ feedIDs: Set<String>, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
|
||||
if webFeedIDs.isEmpty {
|
||||
if feedIDs.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let parameters = feedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
var whereClause = "feedID in \(placeholders) and read=0"
|
||||
if let limit = limit {
|
||||
whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)")
|
||||
@@ -845,8 +845,8 @@ private extension ArticlesTable {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchArticlesForFeedID(_ webFeedID: String, _ database: FMDatabase) -> Set<Article> {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject])
|
||||
func fetchArticlesForFeedID(_ feedID: String, _ database: FMDatabase) -> Set<Article> {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject])
|
||||
}
|
||||
|
||||
func fetchArticles(articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||
@@ -859,15 +859,15 @@ private extension ArticlesTable {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
|
||||
func fetchArticlesSince(_ feedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?)
|
||||
//
|
||||
// datePublished may be nil, so we fall back to dateArrived.
|
||||
if webFeedIDs.isEmpty {
|
||||
if feedIDs.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject]
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let parameters = feedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject]
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
var whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))"
|
||||
if let limit = limit {
|
||||
whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)")
|
||||
@@ -875,13 +875,13 @@ private extension ArticlesTable {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
|
||||
func fetchStarredArticles(_ feedIDs: Set<String>, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1;
|
||||
if webFeedIDs.isEmpty {
|
||||
if feedIDs.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let parameters = feedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
var whereClause = "feedID in \(placeholders) and starred=1"
|
||||
if let limit = limit {
|
||||
whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)")
|
||||
@@ -889,21 +889,21 @@ private extension ArticlesTable {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchStarredArticlesCount(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Int {
|
||||
func fetchStarredArticlesCount(_ feedIDs: Set<String>, _ database: FMDatabase) -> Int {
|
||||
// select count from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1;
|
||||
if webFeedIDs.isEmpty {
|
||||
if feedIDs.isEmpty {
|
||||
return 0
|
||||
}
|
||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let parameters = feedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
let whereClause = "feedID in \(placeholders) and starred=1"
|
||||
return fetchArticleCountsWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||
func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||
let articles = fetchArticlesMatching(searchString, database)
|
||||
// TODO: include the feedIDs in the SQL rather than filtering here.
|
||||
return articles.filter{ webFeedIDs.contains($0.webFeedID) }
|
||||
return articles.filter{ feedIDs.contains($0.feedID) }
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||
|
||||
@@ -19,7 +19,7 @@ extension Article {
|
||||
assertionFailure("Expected articleID.")
|
||||
return nil
|
||||
}
|
||||
guard let webFeedID = row.string(forColumn: DatabaseKey.feedID) else {
|
||||
guard let feedID = row.string(forColumn: DatabaseKey.feedID) else {
|
||||
assertionFailure("Expected feedID.")
|
||||
return nil
|
||||
}
|
||||
@@ -38,10 +38,10 @@ extension Article {
|
||||
let datePublished = row.date(forColumn: DatabaseKey.datePublished)
|
||||
let dateModified = row.date(forColumn: DatabaseKey.dateModified)
|
||||
|
||||
self.init(accountID: accountID, articleID: articleID, webFeedID: webFeedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, datePublished: datePublished, dateModified: dateModified, authors: nil, status: status)
|
||||
self.init(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, datePublished: datePublished, dateModified: dateModified, authors: nil, status: status)
|
||||
}
|
||||
|
||||
init(parsedItem: ParsedItem, maximumDateAllowed: Date, accountID: String, webFeedID: String, status: ArticleStatus) {
|
||||
init(parsedItem: ParsedItem, maximumDateAllowed: Date, accountID: String, feedID: String, status: ArticleStatus) {
|
||||
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
|
||||
|
||||
// Deal with future datePublished and dateModified dates.
|
||||
@@ -58,7 +58,7 @@ extension Article {
|
||||
dateModified = nil
|
||||
}
|
||||
|
||||
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, webFeedID: webFeedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, status: status)
|
||||
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, status: status)
|
||||
}
|
||||
|
||||
private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath<Article,String?>, _ otherArticle: Article, _ key: String, _ dictionary: inout DatabaseDictionary) {
|
||||
@@ -71,7 +71,7 @@ extension Article {
|
||||
if authors.isEmpty {
|
||||
return self
|
||||
}
|
||||
return Article(accountID: self.accountID, articleID: self.articleID, webFeedID: self.webFeedID, uniqueID: self.uniqueID, title: self.title, contentHTML: self.contentHTML, contentText: self.contentText, url: self.rawLink, externalURL: self.rawExternalLink, summary: self.summary, imageURL: self.rawImageLink, datePublished: self.datePublished, dateModified: self.dateModified, authors: authors, status: self.status)
|
||||
return Article(accountID: self.accountID, articleID: self.articleID, feedID: self.feedID, uniqueID: self.uniqueID, title: self.title, contentHTML: self.contentHTML, contentText: self.contentText, url: self.rawLink, externalURL: self.rawExternalLink, summary: self.summary, imageURL: self.rawImageLink, datePublished: self.datePublished, dateModified: self.dateModified, authors: authors, status: self.status)
|
||||
}
|
||||
|
||||
func changesFrom(_ existingArticle: Article) -> DatabaseDictionary? {
|
||||
@@ -117,22 +117,22 @@ extension Article {
|
||||
return Date().addingTimeInterval(60 * 60 * 24) // Allow dates up to about 24 hours ahead of now
|
||||
}
|
||||
|
||||
static func articlesWithWebFeedIDsAndItems(_ webFeedIDsAndItems: [String: Set<ParsedItem>], _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
|
||||
static func articlesWithFeedIDsAndItems(_ feedIDsAndItems: [String: Set<ParsedItem>], _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
|
||||
let maximumDateAllowed = _maximumDateAllowed()
|
||||
var feedArticles = Set<Article>()
|
||||
for (webFeedID, parsedItems) in webFeedIDsAndItems {
|
||||
for (feedID, parsedItems) in feedIDsAndItems {
|
||||
for parsedItem in parsedItems {
|
||||
let status = statusesDictionary[parsedItem.articleID]!
|
||||
let article = Article(parsedItem: parsedItem, maximumDateAllowed: maximumDateAllowed, accountID: accountID, webFeedID: webFeedID, status: status)
|
||||
let article = Article(parsedItem: parsedItem, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: status)
|
||||
feedArticles.insert(article)
|
||||
}
|
||||
}
|
||||
return feedArticles
|
||||
}
|
||||
|
||||
static func articlesWithParsedItems(_ parsedItems: Set<ParsedItem>, _ webFeedID: String, _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
|
||||
static func articlesWithParsedItems(_ parsedItems: Set<ParsedItem>, _ feedID: String, _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
|
||||
let maximumDateAllowed = _maximumDateAllowed()
|
||||
return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, webFeedID: webFeedID, status: statusesDictionary[$0.articleID]!) })
|
||||
return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ extension Article: @retroactive DatabaseObject {
|
||||
var d = DatabaseDictionary()
|
||||
|
||||
d[DatabaseKey.articleID] = articleID
|
||||
d[DatabaseKey.feedID] = webFeedID
|
||||
d[DatabaseKey.feedID] = feedID
|
||||
d[DatabaseKey.uniqueID] = uniqueID
|
||||
|
||||
if let title = title {
|
||||
|
||||
@@ -17,6 +17,6 @@ extension ParsedItem {
|
||||
return s
|
||||
}
|
||||
// Must be same calculation as for Article.
|
||||
return Article.calculatedArticleID(webFeedID: feedURL, uniqueID: uniqueID)
|
||||
return Article.calculatedArticleID(feedID: feedURL, uniqueID: uniqueID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ private extension FetchAllUnreadCountsOperation {
|
||||
return
|
||||
}
|
||||
let unreadCount = resultSet.long(forColumnIndex: 1)
|
||||
if let webFeedID = resultSet.string(forColumnIndex: 0) {
|
||||
unreadCountDictionary[webFeedID] = unreadCount
|
||||
if let feedID = resultSet.string(forColumnIndex: 0) {
|
||||
unreadCountDictionary[feedID] = unreadCount
|
||||
}
|
||||
}
|
||||
resultSet.close()
|
||||
|
||||
@@ -25,10 +25,10 @@ public final class FetchFeedUnreadCountOperation: MainThreadOperation {
|
||||
|
||||
private let queue: DatabaseQueue
|
||||
private let cutoffDate: Date
|
||||
private let webFeedID: String
|
||||
private let feedID: String
|
||||
|
||||
init(webFeedID: String, databaseQueue: DatabaseQueue, cutoffDate: Date) {
|
||||
self.webFeedID = webFeedID
|
||||
init(feedID: String, databaseQueue: DatabaseQueue, cutoffDate: Date) {
|
||||
self.feedID = feedID
|
||||
self.queue = databaseQueue
|
||||
self.cutoffDate = cutoffDate
|
||||
}
|
||||
@@ -55,7 +55,7 @@ private extension FetchFeedUnreadCountOperation {
|
||||
func fetchUnreadCount(_ database: FMDatabase) {
|
||||
let sql = "select count(*) from articles natural join statuses where feedID=? and read=0;"
|
||||
|
||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [webFeedID]) else {
|
||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [feedID]) else {
|
||||
informOperationDelegateOfCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation {
|
||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||
|
||||
private let queue: DatabaseQueue
|
||||
private let webFeedIDs: Set<String>
|
||||
private let feedIDs: Set<String>
|
||||
|
||||
init(webFeedIDs: Set<String>, databaseQueue: DatabaseQueue) {
|
||||
self.webFeedIDs = webFeedIDs
|
||||
init(feedIDs: Set<String>, databaseQueue: DatabaseQueue) {
|
||||
self.feedIDs = feedIDs
|
||||
self.queue = databaseQueue
|
||||
}
|
||||
|
||||
@@ -51,10 +51,10 @@ public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation {
|
||||
private extension FetchUnreadCountsForFeedsOperation {
|
||||
|
||||
func fetchUnreadCounts(_ database: FMDatabase) {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||
let sql = "select distinct feedID, count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 group by feedID;"
|
||||
|
||||
let parameters = Array(webFeedIDs) as [Any]
|
||||
let parameters = Array(feedIDs) as [Any]
|
||||
|
||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
|
||||
informOperationDelegateOfCompletion()
|
||||
@@ -74,8 +74,8 @@ private extension FetchUnreadCountsForFeedsOperation {
|
||||
return
|
||||
}
|
||||
let unreadCount = resultSet.long(forColumnIndex: 1)
|
||||
if let webFeedID = resultSet.string(forColumnIndex: 0) {
|
||||
unreadCountDictionary[webFeedID] = unreadCount
|
||||
if let feedID = resultSet.string(forColumnIndex: 0) {
|
||||
unreadCountDictionary[feedID] = unreadCount
|
||||
}
|
||||
}
|
||||
resultSet.close()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AddWebFeedWindowController" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AddFeedWindowController" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="addButton" destination="dtI-Hu-rFb" id="D11-zR-dWH"/>
|
||||
<outlet property="folderPopupButton" destination="6vt-DL-mVR" id="98M-xt-ZYU"/>
|
||||
@@ -20,21 +20,21 @@
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="217"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
|
||||
<view key="contentView" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="216"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||
<view key="contentView" misplaced="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="3" width="480" height="214"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hVI-F6-nNT">
|
||||
<rect key="frame" x="33" y="180" width="35" height="16"/>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hVI-F6-nNT">
|
||||
<rect key="frame" x="33" y="178" width="35" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="URL:" id="8jE-9v-BT2">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gbr-mI-Uzj" userLabel="URL Text Field">
|
||||
<rect key="frame" x="74" y="123" width="386" height="73"/>
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gbr-mI-Uzj" userLabel="URL Text Field">
|
||||
<rect key="frame" x="74" y="121" width="386" height="73"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="386" id="Wfx-Jk-wQ0"/>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="73" id="x84-xj-BzJ"/>
|
||||
@@ -48,24 +48,24 @@
|
||||
<outlet property="delegate" destination="-2" id="gcI-CI-e5I"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sM9-DX-M0c">
|
||||
<rect key="frame" x="22" y="95" width="46" height="16"/>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sM9-DX-M0c">
|
||||
<rect key="frame" x="22" y="93" width="46" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Name:" id="8ca-Qp-BkT">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TzV-3k-fXd" userLabel="Name Text Field">
|
||||
<rect key="frame" x="74" y="92" width="386" height="21"/>
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TzV-3k-fXd" userLabel="Name Text Field">
|
||||
<rect key="frame" x="74" y="90" width="386" height="21"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="Optional" drawsBackground="YES" usesSingleLineMode="YES" id="pLP-pL-5R5">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dNV-oD-vzR">
|
||||
<rect key="frame" x="18" y="64" width="50" height="16"/>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dNV-oD-vzR">
|
||||
<rect key="frame" x="18" y="63" width="50" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Folder:" id="Kwx-7B-CIu">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -73,10 +73,10 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="6vt-DL-mVR" userLabel="Folder Popup">
|
||||
<rect key="frame" x="72" y="58" width="391" height="25"/>
|
||||
<rect key="frame" x="71" y="56" width="393" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="tLJ-zY-CcZ" id="0cM-5q-Snl">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="OpL-Uf-woJ">
|
||||
<items>
|
||||
<menuItem title="Item 1" state="on" id="tLJ-zY-CcZ"/>
|
||||
@@ -87,7 +87,7 @@
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<button hidden="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QcY-PB-8Y0">
|
||||
<rect key="frame" x="68" y="13" width="166" height="32"/>
|
||||
<rect key="frame" x="67" y="13" width="160" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Open Feed Directory" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="wKl-a9-7FY">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -97,7 +97,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hXq-IS-19x">
|
||||
<rect key="frame" x="302" y="13" width="82" height="32"/>
|
||||
<rect key="frame" x="317" y="13" width="76" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Dop-HC-6Q9">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -110,7 +110,7 @@ Gw
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dtI-Hu-rFb">
|
||||
<rect key="frame" x="384" y="13" width="82" height="32"/>
|
||||
<rect key="frame" x="391" y="13" width="76" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Add" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6NK-Ql-drk">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
|
||||
@@ -11,7 +11,7 @@ import Articles
|
||||
import Account
|
||||
import UserNotifications
|
||||
|
||||
final class WebFeedInspectorViewController: NSViewController, Inspector {
|
||||
final class FeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet weak var iconView: IconView!
|
||||
@IBOutlet weak var nameTextField: NSTextField?
|
||||
@@ -35,7 +35,7 @@ final class WebFeedInspectorViewController: NSViewController, Inspector {
|
||||
let isFallbackInspector = false
|
||||
var objects: [Any]? {
|
||||
didSet {
|
||||
renameWebFeedIfNecessary()
|
||||
renameFeedIfNecessary()
|
||||
updateFeed()
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ final class WebFeedInspectorViewController: NSViewController, Inspector {
|
||||
}
|
||||
|
||||
override func viewDidDisappear() {
|
||||
renameWebFeedIfNecessary()
|
||||
renameFeedIfNecessary()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
@@ -112,15 +112,15 @@ final class WebFeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
}
|
||||
|
||||
extension WebFeedInspectorViewController: NSTextFieldDelegate {
|
||||
extension FeedInspectorViewController: NSTextFieldDelegate {
|
||||
|
||||
func controlTextDidEndEditing(_ note: Notification) {
|
||||
renameWebFeedIfNecessary()
|
||||
renameFeedIfNecessary()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension WebFeedInspectorViewController {
|
||||
private extension FeedInspectorViewController {
|
||||
|
||||
func updateFeed() {
|
||||
guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? Feed else {
|
||||
@@ -202,7 +202,7 @@ private extension WebFeedInspectorViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func renameWebFeedIfNecessary() {
|
||||
func renameFeedIfNecessary() {
|
||||
guard let feed = feed,
|
||||
let account = feed.account,
|
||||
let nameTextField = nameTextField,
|
||||
@@ -210,7 +210,7 @@ private extension WebFeedInspectorViewController {
|
||||
return
|
||||
}
|
||||
|
||||
account.renameWebFeed(feed, to: nameTextField.stringValue) { [weak self] result in
|
||||
account.renameFeed(feed, to: nameTextField.stringValue) { [weak self] result in
|
||||
if case .failure(let error) = result {
|
||||
self?.presentError(error)
|
||||
} else {
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -32,12 +31,12 @@
|
||||
<!--Feed-->
|
||||
<scene sceneID="vUh-Rc-fPi">
|
||||
<objects>
|
||||
<viewController title="Feed" storyboardIdentifier="Feed" showSeguePresentationStyle="single" id="sfH-oR-GXm" customClass="WebFeedInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController title="Feed" storyboardIdentifier="Feed" showSeguePresentationStyle="single" id="sfH-oR-GXm" customClass="FeedInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="ecA-UY-KEd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="273" height="322"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IWu-80-XC5">
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IWu-80-XC5">
|
||||
<rect key="frame" x="20" y="190" width="233" height="56"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="56" id="zV3-AX-gyC"/>
|
||||
@@ -54,7 +53,7 @@ Field</string>
|
||||
<outlet property="delegate" destination="sfH-oR-GXm" id="Dd0-5H-8HH"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2WO-Iu-p5e">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2WO-Iu-p5e">
|
||||
<rect key="frame" x="18" y="96" width="237" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" title="Home Page" usesSingleLineMode="YES" id="Fg8-rA-G5J">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -62,7 +61,7 @@ Field</string>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zm0-15-BFy">
|
||||
<textField focusRingType="none" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zm0-15-BFy">
|
||||
<rect key="frame" x="18" y="76" width="237" height="16"/>
|
||||
<textFieldCell key="cell" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" title="http://example.com/" id="L2p-ur-j7a">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -70,7 +69,7 @@ Field</string>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ju6-Zo-8X4">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ju6-Zo-8X4">
|
||||
<rect key="frame" x="18" y="40" width="237" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" title="Feed" usesSingleLineMode="YES" id="zzB-rX-1dK">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -78,7 +77,7 @@ Field</string>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Vvk-KG-JlG">
|
||||
<textField focusRingType="none" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Vvk-KG-JlG">
|
||||
<rect key="frame" x="18" y="20" width="237" height="16"/>
|
||||
<textFieldCell key="cell" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" title="http://example.com/feed" id="HpC-rK-YGK">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -169,7 +168,7 @@ Field</string>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="NSFolder" id="C4n-vS-297"/>
|
||||
</imageView>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="jHf-rc-GNr" userLabel="Folder Name Field">
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="jHf-rc-GNr" userLabel="Folder Name Field">
|
||||
<rect key="frame" x="20" y="20" width="227" height="56"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="56" id="Ele-JD-mB2"/>
|
||||
@@ -221,7 +220,7 @@ Field</string>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="NSSmartBadgeTemplate" id="Z52-bd-Lgz"/>
|
||||
</imageView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Xp-FX-kn3">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Xp-FX-kn3">
|
||||
<rect key="frame" x="18" y="20" width="231" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Label" id="3v9-Z7-d7l">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -256,7 +255,7 @@ Field</string>
|
||||
<rect key="frame" x="0.0" y="0.0" width="267" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="icb-M6-R2N">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="icb-M6-R2N">
|
||||
<rect key="frame" x="18" y="20" width="231" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Nothing to inspect" id="iLD-8q-EAJ">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -264,7 +263,7 @@ Field</string>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zQp-oc-Qtc">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zQp-oc-Qtc">
|
||||
<rect key="frame" x="18" y="20" width="231" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Multiple selection" id="5oG-0x-T8O">
|
||||
<font key="font" metaFont="system"/>
|
||||
|
||||
@@ -37,7 +37,7 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
let folderTreeControllerDelegate = FolderTreeControllerDelegate()
|
||||
let folderTreeController = TreeController(delegate: folderTreeControllerDelegate)
|
||||
|
||||
addFeedWindowController = AddWebFeedWindowController(urlString: urlString ?? urlStringFromPasteboard,
|
||||
addFeedWindowController = AddFeedWindowController(urlString: urlString ?? urlStringFromPasteboard,
|
||||
name: name,
|
||||
account: account,
|
||||
folder: folder,
|
||||
|
||||
@@ -16,8 +16,8 @@ protocol AddFeedWindowControllerDelegate: AnyObject {
|
||||
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
|
||||
}
|
||||
|
||||
protocol AddFeedWindowController {
|
||||
|
||||
var window: NSWindow? { get }
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow)
|
||||
}
|
||||
//protocol AddFeedWindowController {
|
||||
//
|
||||
// var window: NSWindow? { get }
|
||||
// func runSheetOnWindow(_ hostWindow: NSWindow)
|
||||
//}
|
||||
|
||||
@@ -12,8 +12,8 @@ import RSTree
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
class AddWebFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
|
||||
final class AddFeedWindowController : NSWindowController {
|
||||
|
||||
@IBOutlet var urlTextField: NSTextField!
|
||||
@IBOutlet var nameTextField: NSTextField!
|
||||
@IBOutlet var addButton: NSButton!
|
||||
@@ -38,7 +38,7 @@ class AddWebFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
var hostWindow: NSWindow!
|
||||
|
||||
convenience init(urlString: String?, name: String?, account: Account?, folder: Folder?, folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
|
||||
self.init(windowNibName: NSNib.Name("AddWebFeedSheet"))
|
||||
self.init(windowNibName: NSNib.Name("AddFeedSheet"))
|
||||
self.urlString = urlString
|
||||
self.initialName = name
|
||||
self.initialAccount = account
|
||||
@@ -117,9 +117,6 @@ class AddWebFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
@objc func controlTextDidChange(_ obj: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
|
||||
private extension AddWebFeedWindowController {
|
||||
|
||||
private func updateUI() {
|
||||
addButton.isEnabled = urlTextField.stringValue.mayBeURL && selectedContainer() != nil
|
||||
|
||||
@@ -130,7 +130,7 @@ final class DetailWebViewController: NSViewController {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webInspectorEnabledDidChange(_:)), name: .WebInspectorEnabledDidChange, object: nil)
|
||||
#endif
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
@@ -141,7 +141,7 @@ final class DetailWebViewController: NSViewController {
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ extension SidebarViewController {
|
||||
|
||||
switch object {
|
||||
case is Feed:
|
||||
return menuForWebFeed(object as! Feed)
|
||||
return menuForFeed(object as! Feed)
|
||||
case is Folder:
|
||||
return menuForFolder(object as! Folder)
|
||||
case is PseudoFeed:
|
||||
@@ -206,34 +206,34 @@ private extension SidebarViewController {
|
||||
return menu
|
||||
}
|
||||
|
||||
func menuForWebFeed(_ webFeed: Feed) -> NSMenu? {
|
||||
func menuForFeed(_ feed: Feed) -> NSMenu? {
|
||||
|
||||
let menu = NSMenu(title: "")
|
||||
|
||||
if webFeed.unreadCount > 0 {
|
||||
menu.addItem(markAllReadMenuItem([webFeed]))
|
||||
if feed.unreadCount > 0 {
|
||||
menu.addItem(markAllReadMenuItem([feed]))
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
if let homePageURL = webFeed.homePageURL, let _ = URL(string: homePageURL) {
|
||||
if let homePageURL = feed.homePageURL, let _ = URL(string: homePageURL) {
|
||||
let item = menuItem(NSLocalizedString("Open Home Page", comment: "Command"), #selector(openHomePageFromContextualMenu(_:)), homePageURL.decodedURLString ?? homePageURL)
|
||||
menu.addItem(item)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), webFeed.url.decodedURLString ?? webFeed.url)
|
||||
let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), feed.url.decodedURLString ?? feed.url)
|
||||
menu.addItem(copyFeedURLItem)
|
||||
|
||||
if let homePageURL = webFeed.homePageURL {
|
||||
if let homePageURL = feed.homePageURL {
|
||||
let item = menuItem(NSLocalizedString("Copy Home Page URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), homePageURL.decodedURLString ?? homePageURL)
|
||||
menu.addItem(item)
|
||||
}
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let notificationText = webFeed.notificationDisplayName.capitalized
|
||||
let notificationText = feed.notificationDisplayName.capitalized
|
||||
|
||||
let notificationMenuItem = menuItem(notificationText, #selector(toggleNotificationsFromContextMenu(_:)), webFeed)
|
||||
if webFeed.isNotifyAboutNewArticles == nil || webFeed.isNotifyAboutNewArticles! == false {
|
||||
let notificationMenuItem = menuItem(notificationText, #selector(toggleNotificationsFromContextMenu(_:)), feed)
|
||||
if feed.isNotifyAboutNewArticles == nil || feed.isNotifyAboutNewArticles! == false {
|
||||
notificationMenuItem.state = .off
|
||||
} else {
|
||||
notificationMenuItem.state = .on
|
||||
@@ -241,9 +241,9 @@ private extension SidebarViewController {
|
||||
menu.addItem(notificationMenuItem)
|
||||
|
||||
let articleExtractorText = NSLocalizedString("Always Use Reader View", comment: "Always Use Reader View")
|
||||
let articleExtractorMenuItem = menuItem(articleExtractorText, #selector(toggleArticleExtractorFromContextMenu(_:)), webFeed)
|
||||
let articleExtractorMenuItem = menuItem(articleExtractorText, #selector(toggleArticleExtractorFromContextMenu(_:)), feed)
|
||||
|
||||
if webFeed.isArticleExtractorAlwaysOn == nil || webFeed.isArticleExtractorAlwaysOn! == false {
|
||||
if feed.isArticleExtractorAlwaysOn == nil || feed.isArticleExtractorAlwaysOn! == false {
|
||||
articleExtractorMenuItem.state = .off
|
||||
} else {
|
||||
articleExtractorMenuItem.state = .on
|
||||
@@ -252,8 +252,8 @@ private extension SidebarViewController {
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
menu.addItem(renameMenuItem(webFeed))
|
||||
menu.addItem(deleteMenuItem([webFeed]))
|
||||
menu.addItem(renameMenuItem(feed))
|
||||
menu.addItem(deleteMenuItem([feed]))
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
@@ -448,8 +448,8 @@ protocol SidebarDelegate: AnyObject {
|
||||
if isReadFiltered, let feedID = feed.sidebarItemID {
|
||||
self.treeControllerDelegate.addFilterException(feedID)
|
||||
|
||||
if let webFeed = feed as? Feed, let account = webFeed.account {
|
||||
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
|
||||
if let feed = feed as? Feed, let account = feed.account {
|
||||
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(feed) })
|
||||
if let parentFolderFeedID = parentFolder?.sidebarItemID {
|
||||
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
||||
}
|
||||
@@ -540,10 +540,10 @@ private extension SidebarViewController {
|
||||
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
}
|
||||
} else if let webFeed = feed as? Feed {
|
||||
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.feedID) != nil {
|
||||
} else if let feed = feed as? Feed {
|
||||
if feed.account?.existingFeed(withFeedID: feed.feedID) != nil {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
addParentFolderToFilterExceptions(webFeed)
|
||||
addParentFolderToFilterExceptions(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -738,10 +738,10 @@ private extension SidebarViewController {
|
||||
}
|
||||
|
||||
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
|
||||
guard let webFeedID = userInfo?[ArticlePathKey.webFeedID] as? String else {
|
||||
guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else {
|
||||
return nil
|
||||
}
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == webFeedID }) {
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -111,7 +111,7 @@ private extension ArticlePasteboardWriter {
|
||||
static let articleID = "articleID" // database ID, unique per account
|
||||
static let uniqueID = "uniqueID" // unique ID, unique per feed (guid, or possibly calculated)
|
||||
static let feedURL = "feedURL"
|
||||
static let webFeedID = "webFeedID" // may differ from feedURL if coming from a syncing system
|
||||
static let feedID = "feedID" // may differ from feedURL if coming from a syncing system
|
||||
static let title = "title"
|
||||
static let contentHTML = "contentHTML"
|
||||
static let contentText = "contentText"
|
||||
@@ -147,7 +147,7 @@ private extension ArticlePasteboardWriter {
|
||||
d[Key.feedURL] = feed.url
|
||||
}
|
||||
|
||||
d[Key.webFeedID] = article.webFeedID
|
||||
d[Key.feedID] = article.feedID
|
||||
d[Key.title] = article.title ?? nil
|
||||
d[Key.contentHTML] = article.contentHTML ?? nil
|
||||
d[Key.contentText] = article.contentText ?? nil
|
||||
|
||||
@@ -141,8 +141,8 @@ extension TimelineContainerViewController: TimelineDelegate {
|
||||
delegate?.timelineSelectionDidChange(self, articles: selectedArticles, mode: mode(for: timelineViewController))
|
||||
}
|
||||
|
||||
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: Feed) {
|
||||
delegate?.timelineRequestedFeedSelection(self, feed: webFeed)
|
||||
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) {
|
||||
delegate?.timelineRequestedFeedSelection(self, feed: feed)
|
||||
}
|
||||
|
||||
func timelineInvalidatedRestorationState(_: TimelineViewController) {
|
||||
|
||||
@@ -65,10 +65,10 @@ extension TimelineViewController {
|
||||
}
|
||||
|
||||
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
|
||||
guard let menuItem = sender as? NSMenuItem, let webFeed = menuItem.representedObject as? Feed else {
|
||||
guard let menuItem = sender as? NSMenuItem, let feed = menuItem.representedObject as? Feed else {
|
||||
return
|
||||
}
|
||||
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
|
||||
delegate?.timelineRequestedFeedSelection(self, feed: feed)
|
||||
}
|
||||
|
||||
@objc func markAllInFeedAsRead(_ sender: Any?) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import os.log
|
||||
|
||||
protocol TimelineDelegate: AnyObject {
|
||||
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: Feed)
|
||||
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
||||
func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
if !didRegisterForNotifications {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
@@ -593,7 +593,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
updateUnreadCount()
|
||||
}
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard showIcons, let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
return
|
||||
}
|
||||
@@ -636,11 +636,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<Feed> else {
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldFetchAndMergeArticles = representedObjectsContainsAnyWebFeed(feeds) || representedObjectsContainsAnyPseudoFeed()
|
||||
let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds) || representedObjectsContainsAnyPseudoFeed()
|
||||
if shouldFetchAndMergeArticles {
|
||||
queueFetchAndMergeArticles()
|
||||
}
|
||||
@@ -724,8 +724,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
||||
let prototypeID = "prototype"
|
||||
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date())
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||
|
||||
let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, featuredImage: nil)
|
||||
let height = TimelineCellLayout.height(for: 100, cellData: prototypeCellData, appearance: cellAppearance)
|
||||
return height
|
||||
@@ -1226,7 +1226,7 @@ private extension TimelineViewController {
|
||||
return representedObjects?.contains(where: { $0 is Folder }) ?? false
|
||||
}
|
||||
|
||||
func representedObjectsContainsAnyWebFeed(_ webFeeds: Set<Feed>) -> Bool {
|
||||
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
@@ -1234,15 +1234,15 @@ private extension TimelineViewController {
|
||||
}
|
||||
for representedObject in representedObjects {
|
||||
if let feed = representedObject as? Feed {
|
||||
for oneFeed in webFeeds {
|
||||
for oneFeed in feeds {
|
||||
if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let folder = representedObject as? Folder {
|
||||
for oneFeed in webFeeds {
|
||||
if folder.hasWebFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
for oneFeed in feeds {
|
||||
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
<element type="account">
|
||||
<cocoa key="accounts"/>
|
||||
</element>
|
||||
<element type="webFeed">
|
||||
<cocoa key="webFeeds"/>
|
||||
<element type="feed">
|
||||
<cocoa key="feeds"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
@@ -88,23 +88,23 @@
|
||||
<property name="active" code="Actv" type="boolean" access="rw" description="Whether or not the account is active">
|
||||
<cocoa key="scriptingIsActive"/>
|
||||
</property>
|
||||
<property name="allWebFeeds" code="Feds" access="r" description="All feeds, including feeds inside folders">
|
||||
<cocoa key="allWebFeeds"/>
|
||||
<type type="webFeed" list="yes"/>
|
||||
<property name="allFeeds" code="Feds" access="r" description="All feeds, including feeds inside folders">
|
||||
<cocoa key="allFeeds"/>
|
||||
<type type="feed" list="yes"/>
|
||||
</property>
|
||||
<property name="opml representation" code="OPML" type="text" access="r" description="OPML representation for the account">
|
||||
<cocoa key="opmlRepresentation"/>
|
||||
</property>
|
||||
<element type="webFeed">
|
||||
<cocoa key="webFeeds"/>
|
||||
<element type="feed">
|
||||
<cocoa key="feeds"/>
|
||||
</element>
|
||||
<element type="folder">
|
||||
<cocoa key="folders"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
<class name="webFeed" code="Feed" plural="webFeeds" description="An RSS feed">
|
||||
<cocoa class="ScriptableWebFeed"/>
|
||||
<class name="feed" code="Feed" plural="feeds" description="An RSS feed">
|
||||
<cocoa class="ScriptableFeed"/>
|
||||
<property name="name" code="pnam" type="text" access="r" description="The name of the feed">
|
||||
<cocoa key="name"/>
|
||||
</property>
|
||||
@@ -165,8 +165,8 @@
|
||||
<property name="opml representation" code="OPML" type="text" access="r" description="OPML representation for the folder">
|
||||
<cocoa key="opmlRepresentation"/>
|
||||
</property>
|
||||
<element type="webFeed">
|
||||
<cocoa key="webFeeds"/>
|
||||
<element type="feed">
|
||||
<cocoa key="feeds"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
<property name="image url" code="IURL" type="text" access="r" description="an image url for the article">
|
||||
<cocoa key="imageURL"/>
|
||||
</property>
|
||||
<property name="feed" code="Feed" type="webFeed" access="r" description="the containing feed">
|
||||
<property name="feed" code="Feed" type="feed" access="r" description="the containing feed">
|
||||
<cocoa key="feed"/>
|
||||
</property>
|
||||
<element type="author">
|
||||
|
||||
@@ -73,7 +73,7 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
account.removeFolder(scriptableFolder.folder) { result in
|
||||
}
|
||||
}
|
||||
} else if let scriptableFeed = element as? ScriptableWebFeed {
|
||||
} else if let scriptableFeed = element as? ScriptableFeed {
|
||||
BatchUpdate.shared.perform {
|
||||
var container: Container? = nil
|
||||
if let scriptableFolder = scriptableFeed.container as? ScriptableFolder {
|
||||
@@ -81,7 +81,7 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
} else {
|
||||
container = account
|
||||
}
|
||||
account.removeWebFeed(scriptableFeed.webFeed, from: container!) { result in
|
||||
account.removeFeed(scriptableFeed.feed, from: container!) { result in
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,22 +94,22 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
|
||||
// MARK: --- Scriptable elements ---
|
||||
|
||||
@objc(webFeeds)
|
||||
var webFeeds:NSArray {
|
||||
return account.topLevelFeeds.map { ScriptableWebFeed($0, container:self) } as NSArray
|
||||
@objc(feeds)
|
||||
var feeds:NSArray {
|
||||
return account.topLevelFeeds.map { ScriptableFeed($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
@objc(valueInWebFeedsWithUniqueID:)
|
||||
func valueInWebFeeds(withUniqueID id:String) -> ScriptableWebFeed? {
|
||||
guard let feed = account.existingWebFeed(withWebFeedID: id) else { return nil }
|
||||
return ScriptableWebFeed(feed, container:self)
|
||||
@objc(valueInFeedsWithUniqueID:)
|
||||
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? {
|
||||
guard let feed = account.existingFeed(withFeedID: id) else { return nil }
|
||||
return ScriptableFeed(feed, container:self)
|
||||
}
|
||||
|
||||
@objc(valueInWebFeedsWithName:)
|
||||
func valueInWebFeeds(withName name:String) -> ScriptableWebFeed? {
|
||||
@objc(valueInFeedsWithName:)
|
||||
func valueInFeeds(withName name:String) -> ScriptableFeed? {
|
||||
let feeds = Array(account.flattenedFeeds())
|
||||
guard let feed = feeds.first(where:{$0.name == name}) else { return nil }
|
||||
return ScriptableWebFeed(feed, container:self)
|
||||
return ScriptableFeed(feed, container:self)
|
||||
}
|
||||
|
||||
@objc(folders)
|
||||
@@ -130,21 +130,21 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
|
||||
// MARK: --- Scriptable properties ---
|
||||
|
||||
@objc(allWebFeeds)
|
||||
var allWebFeeds: NSArray {
|
||||
var webFeeds = [ScriptableWebFeed]()
|
||||
for webFeed in account.topLevelFeeds {
|
||||
webFeeds.append(ScriptableWebFeed(webFeed, container: self))
|
||||
@objc(allFeeds)
|
||||
var allFeeds: NSArray {
|
||||
var feeds = [ScriptableFeed]()
|
||||
for feed in account.topLevelFeeds {
|
||||
feeds.append(ScriptableFeed(feed, container: self))
|
||||
}
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
let scriptableFolder = ScriptableFolder(folder, container: self)
|
||||
for webFeed in folder.topLevelFeeds {
|
||||
webFeeds.append(ScriptableWebFeed(webFeed, container: scriptableFolder))
|
||||
for feed in folder.topLevelFeeds {
|
||||
feeds.append(ScriptableFeed(feed, container: scriptableFolder))
|
||||
}
|
||||
}
|
||||
}
|
||||
return webFeeds as NSArray
|
||||
return feeds as NSArray
|
||||
}
|
||||
|
||||
@objc(opmlRepresentation)
|
||||
|
||||
@@ -94,8 +94,8 @@ extension AppDelegate : AppDelegateAppleEvents {
|
||||
class NetNewsWireCreateElementCommand : NSCreateCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
let classDescription = self.createClassDescription
|
||||
if (classDescription.className == "webFeed") {
|
||||
return ScriptableWebFeed.handleCreateElement(command:self)
|
||||
if (classDescription.className == "feed") {
|
||||
return ScriptableFeed.handleCreateElement(command:self)
|
||||
} else if (classDescription.className == "folder") {
|
||||
return ScriptableFolder.handleCreateElement(command:self)
|
||||
}
|
||||
|
||||
@@ -142,12 +142,12 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
}
|
||||
|
||||
@objc(feed)
|
||||
var feed: ScriptableWebFeed? {
|
||||
var feed: ScriptableFeed? {
|
||||
guard let parentFeed = self.article.feed,
|
||||
let account = parentFeed.account
|
||||
else { return nil }
|
||||
|
||||
return ScriptableWebFeed(parentFeed, container: ScriptableAccount(account))
|
||||
return ScriptableFeed(parentFeed, container: ScriptableAccount(account))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
||||
}
|
||||
|
||||
func deleteElement(_ element:ScriptingObject) {
|
||||
if let scriptableFeed = element as? ScriptableWebFeed {
|
||||
if let scriptableFeed = element as? ScriptableFeed {
|
||||
BatchUpdate.shared.perform {
|
||||
folder.account?.removeWebFeed(scriptableFeed.webFeed, from: folder) { result in }
|
||||
folder.account?.removeFeed(scriptableFeed.feed, from: folder) { result in }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,10 +95,10 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
||||
|
||||
// MARK: --- Scriptable elements ---
|
||||
|
||||
@objc(webFeeds)
|
||||
var webFeeds:NSArray {
|
||||
@objc(feeds)
|
||||
var feeds:NSArray {
|
||||
let feeds = Array(folder.topLevelFeeds)
|
||||
return feeds.map { ScriptableWebFeed($0, container:self) } as NSArray
|
||||
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
// MARK: --- Scriptable properties ---
|
||||
|
||||
@@ -31,7 +31,7 @@ extension NSApplication : ScriptingObjectContainer {
|
||||
var scriptableArticle: ScriptableArticle?
|
||||
if let currentArticle = appDelegate.scriptingCurrentArticle {
|
||||
if let feed = currentArticle.feed {
|
||||
let scriptableFeed = ScriptableWebFeed(feed, container:self)
|
||||
let scriptableFeed = ScriptableFeed(feed, container:self)
|
||||
scriptableArticle = ScriptableArticle(currentArticle, container:scriptableFeed)
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ extension NSApplication : ScriptingObjectContainer {
|
||||
let articles = appDelegate.scriptingSelectedArticles
|
||||
let scriptableArticles:[ScriptableArticle] = articles.compactMap { article in
|
||||
if let feed = article.feed {
|
||||
let scriptableFeed = ScriptableWebFeed(feed, container:self)
|
||||
let scriptableFeed = ScriptableFeed(feed, container:self)
|
||||
return ScriptableArticle(article, container:scriptableFeed)
|
||||
} else {
|
||||
return nil
|
||||
@@ -73,7 +73,7 @@ extension NSApplication : ScriptingObjectContainer {
|
||||
for 'articles of feed "The Shape of Everything" of account "On My Mac"'
|
||||
*/
|
||||
|
||||
func allWebFeeds() -> [Feed] {
|
||||
func allFeeds() -> [Feed] {
|
||||
let accounts = AccountManager.shared.activeAccounts
|
||||
let emptyFeeds:[Feed] = []
|
||||
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in
|
||||
@@ -82,17 +82,17 @@ extension NSApplication : ScriptingObjectContainer {
|
||||
}
|
||||
}
|
||||
|
||||
@objc(webFeeds)
|
||||
func webFeeds() -> NSArray {
|
||||
let webFeeds = self.allWebFeeds()
|
||||
return webFeeds.map { ScriptableWebFeed($0, container:self) } as NSArray
|
||||
@objc(feeds)
|
||||
func feeds() -> NSArray {
|
||||
let feeds = self.allFeeds()
|
||||
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
@objc(valueInWebFeedsWithUniqueID:)
|
||||
func valueInWebFeeds(withUniqueID id:String) -> ScriptableWebFeed? {
|
||||
let webFeeds = self.allWebFeeds()
|
||||
guard let webFeed = webFeeds.first(where:{$0.feedID == id}) else { return nil }
|
||||
return ScriptableWebFeed(webFeed, container:self)
|
||||
@objc(valueInFeedsWithUniqueID:)
|
||||
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? {
|
||||
let feeds = self.allFeeds()
|
||||
guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil }
|
||||
return ScriptableFeed(feed, container:self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ import RSParser
|
||||
import Account
|
||||
import Articles
|
||||
|
||||
@objc(ScriptableWebFeed)
|
||||
class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
@objc(ScriptableFeed)
|
||||
class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
|
||||
let webFeed:Feed
|
||||
let container:ScriptingObjectContainer
|
||||
let feed: Feed
|
||||
let container: ScriptingObjectContainer
|
||||
|
||||
init (_ webFeed:Feed, container:ScriptingObjectContainer) {
|
||||
self.webFeed = webFeed
|
||||
init (_ feed:Feed, container:ScriptingObjectContainer) {
|
||||
self.feed = feed
|
||||
self.container = container
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
// MARK: --- ScriptingObject protocol ---
|
||||
|
||||
var scriptingKey: String {
|
||||
return "webFeeds"
|
||||
return "feeds"
|
||||
}
|
||||
|
||||
// MARK: --- UniqueIdScriptingObject protocol ---
|
||||
@@ -45,7 +45,7 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
|
||||
@objc(uniqueId)
|
||||
var scriptingUniqueId:Any {
|
||||
return webFeed.feedID
|
||||
return feed.feedID
|
||||
}
|
||||
|
||||
// MARK: --- ScriptingObjectContainer protocol ---
|
||||
@@ -71,13 +71,13 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
return url
|
||||
}
|
||||
|
||||
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableWebFeed {
|
||||
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
|
||||
let scriptableAccount = ScriptableAccount(account)
|
||||
if let folder = folder {
|
||||
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
|
||||
return ScriptableWebFeed(feed, container:scriptableFolder)
|
||||
return ScriptableFeed(feed, container:scriptableFolder)
|
||||
} else {
|
||||
return ScriptableWebFeed(feed, container:scriptableAccount)
|
||||
return ScriptableFeed(feed, container:scriptableAccount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,51 +121,51 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
|
||||
@objc(url)
|
||||
var url:String {
|
||||
return self.webFeed.url
|
||||
return self.feed.url
|
||||
}
|
||||
|
||||
@objc(name)
|
||||
var name:String {
|
||||
return self.webFeed.name ?? ""
|
||||
return self.feed.name ?? ""
|
||||
}
|
||||
|
||||
@objc(homePageURL)
|
||||
var homePageURL:String {
|
||||
return self.webFeed.homePageURL ?? ""
|
||||
return self.feed.homePageURL ?? ""
|
||||
}
|
||||
|
||||
@objc(iconURL)
|
||||
var iconURL:String {
|
||||
return self.webFeed.iconURL ?? ""
|
||||
return self.feed.iconURL ?? ""
|
||||
}
|
||||
|
||||
@objc(faviconURL)
|
||||
var faviconURL:String {
|
||||
return self.webFeed.faviconURL ?? ""
|
||||
return self.feed.faviconURL ?? ""
|
||||
}
|
||||
|
||||
@objc(opmlRepresentation)
|
||||
var opmlRepresentation:String {
|
||||
return self.webFeed.OPMLString(indentLevel:0)
|
||||
return self.feed.OPMLString(indentLevel:0)
|
||||
}
|
||||
|
||||
// MARK: --- scriptable elements ---
|
||||
|
||||
@objc(authors)
|
||||
var authors:NSArray {
|
||||
let feedAuthors = webFeed.authors ?? []
|
||||
let feedAuthors = feed.authors ?? []
|
||||
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
@objc(valueInAuthorsWithUniqueID:)
|
||||
func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
|
||||
guard let author = webFeed.authors?.first(where:{$0.authorID == id}) else { return nil }
|
||||
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
|
||||
return ScriptableAuthor(author, container:self)
|
||||
}
|
||||
|
||||
@objc(articles)
|
||||
var articles:NSArray {
|
||||
let feedArticles = (try? webFeed.fetchArticles()) ?? Set<Article>()
|
||||
let feedArticles = (try? feed.fetchArticles()) ?? Set<Article>()
|
||||
// the articles are a set, use the sorting algorithm from the viewer
|
||||
let sortedArticles = feedArticles.sorted(by:{
|
||||
return $0.logicalDatePublished > $1.logicalDatePublished
|
||||
@@ -175,7 +175,7 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
|
||||
@objc(valueInArticlesWithUniqueID:)
|
||||
func valueInArticles(withUniqueID id:String) -> ScriptableArticle? {
|
||||
let articles = (try? webFeed.fetchArticles()) ?? Set<Article>()
|
||||
let articles = (try? feed.fetchArticles()) ?? Set<Article>()
|
||||
guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil }
|
||||
return ScriptableArticle(article, container:self)
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
513F32812593EF180003048F /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 516B695E24D2F33B00B5702F /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
513F32882593EF8F0003048F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; };
|
||||
513F32892593EF8F0003048F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */; };
|
||||
5141E7392373C18B0013FF27 /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7382373C18B0013FF27 /* FeedInspectorViewController.swift */; };
|
||||
5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142192923522B5500E07E2C /* ImageViewController.swift */; };
|
||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514219362352510100E07E2C /* ImageScrollView.swift */; };
|
||||
5142194B2353C1CF00E07E2C /* main_mac.js in Resources */ = {isa = PBXBuildFile; fileRef = 5142194A2353C1CF00E07E2C /* main_mac.js */; };
|
||||
@@ -427,7 +427,7 @@
|
||||
845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; };
|
||||
845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; };
|
||||
84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
||||
8472058120142E8900AD578B /* WebFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* WebFeedInspectorViewController.swift */; };
|
||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; };
|
||||
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; };
|
||||
847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; };
|
||||
847E64A02262783000E00365 /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */; };
|
||||
@@ -861,7 +861,7 @@
|
||||
513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
5141E7382373C18B0013FF27 /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailIconSchemeHandler.swift; sourceTree = "<group>"; };
|
||||
5142192923522B5500E07E2C /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||
514219362352510100E07E2C /* ImageScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageScrollView.swift; sourceTree = "<group>"; };
|
||||
@@ -1068,7 +1068,7 @@
|
||||
845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarredFeedDelegate.swift; sourceTree = "<group>"; };
|
||||
845EE7C01FC2488C00854A1F /* SmartFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeed.swift; sourceTree = "<group>"; };
|
||||
84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkStatusCommand.swift; sourceTree = "<group>"; };
|
||||
8472058020142E8900AD578B /* WebFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
|
||||
8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedDelegate.swift; sourceTree = "<group>"; };
|
||||
847CD6C9232F4CBF00FAC46D /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
|
||||
@@ -1430,7 +1430,7 @@
|
||||
516A09412361248000EAE89B /* Inspector.storyboard */,
|
||||
51A16991235E10D600EB091F /* AccountInspectorViewController.swift */,
|
||||
5110C37C2373A8D100A9C04F /* InspectorIconHeaderView.swift */,
|
||||
5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */,
|
||||
5141E7382373C18B0013FF27 /* FeedInspectorViewController.swift */,
|
||||
);
|
||||
path = Inspector;
|
||||
sourceTree = "<group>";
|
||||
@@ -2068,7 +2068,7 @@
|
||||
children = (
|
||||
84BBB12B20142A4700F054F5 /* Inspector.storyboard */,
|
||||
84BBB12C20142A4700F054F5 /* InspectorWindowController.swift */,
|
||||
8472058020142E8900AD578B /* WebFeedInspectorViewController.swift */,
|
||||
8472058020142E8900AD578B /* FeedInspectorViewController.swift */,
|
||||
841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */,
|
||||
841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */,
|
||||
841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */,
|
||||
@@ -3206,7 +3206,7 @@
|
||||
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
|
||||
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */,
|
||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
||||
5141E7392373C18B0013FF27 /* FeedInspectorViewController.swift in Sources */,
|
||||
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */,
|
||||
5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */,
|
||||
84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
|
||||
@@ -3354,7 +3354,7 @@
|
||||
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */,
|
||||
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */,
|
||||
51FE100A234673A00056195D /* ActivityManager.swift in Sources */,
|
||||
8472058120142E8900AD578B /* WebFeedInspectorViewController.swift in Sources */,
|
||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
||||
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */,
|
||||
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
||||
84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */,
|
||||
|
||||
@@ -39,7 +39,7 @@ class ActivityManager {
|
||||
}
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
}
|
||||
|
||||
func invalidateCurrentActivities() {
|
||||
@@ -53,8 +53,8 @@ class ActivityManager {
|
||||
|
||||
selectingActivity = makeSelectFeedActivity(feed: feed)
|
||||
|
||||
if let webFeed = feed as? Feed {
|
||||
updateSelectingActivityFeedSearchAttributes(with: webFeed)
|
||||
if let feed = feed as? Feed {
|
||||
updateSelectingActivityFeedSearchAttributes(with: feed)
|
||||
}
|
||||
|
||||
donate(selectingActivity!)
|
||||
@@ -116,8 +116,8 @@ class ActivityManager {
|
||||
}
|
||||
}
|
||||
|
||||
for webFeed in account.flattenedFeeds() {
|
||||
ids.append(contentsOf: identifiers(for: webFeed))
|
||||
for feed in account.flattenedFeeds() {
|
||||
ids.append(contentsOf: identifiers(for: feed))
|
||||
}
|
||||
|
||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
||||
@@ -127,31 +127,31 @@ class ActivityManager {
|
||||
var ids = [String]()
|
||||
ids.append(identifier(for: folder))
|
||||
|
||||
for webFeed in folder.flattenedFeeds() {
|
||||
ids.append(contentsOf: identifiers(for: webFeed))
|
||||
for feed in folder.flattenedFeeds() {
|
||||
ids.append(contentsOf: identifiers(for: feed))
|
||||
}
|
||||
|
||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
||||
}
|
||||
|
||||
static func cleanUp(_ webFeed: Feed) {
|
||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: webFeed))
|
||||
static func cleanUp(_ feed: Feed) {
|
||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: feed))
|
||||
}
|
||||
#endif
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let webFeed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.webFeedID] as? String else {
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.feedID] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
if let article = readingArticle, activityFeedId == article.webFeedID {
|
||||
if let article = readingArticle, activityFeedId == article.feedID {
|
||||
updateReadArticleSearchAttributes(with: article)
|
||||
}
|
||||
#endif
|
||||
|
||||
if activityFeedId == webFeed.feedID {
|
||||
updateSelectingActivityFeedSearchAttributes(with: webFeed)
|
||||
if activityFeedId == feed.feedID {
|
||||
updateSelectingActivityFeedSearchAttributes(with: feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ private extension ActivityManager {
|
||||
}
|
||||
|
||||
static func identifier(for article: Article) -> String {
|
||||
return "account_\(article.accountID)_feed_\(article.webFeedID)_article_\(article.articleID)"
|
||||
return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)"
|
||||
}
|
||||
|
||||
static func identifiers(for feed: Feed) -> [String] {
|
||||
|
||||
@@ -98,7 +98,7 @@ private struct SidebarItemSpecifier {
|
||||
private weak var account: Account?
|
||||
private let parentFolder: Folder?
|
||||
private let folder: Folder?
|
||||
private let webFeed: Feed?
|
||||
private let feed: Feed?
|
||||
private let path: ContainerPath
|
||||
private let errorHandler: (Error) -> ()
|
||||
|
||||
@@ -118,13 +118,13 @@ private struct SidebarItemSpecifier {
|
||||
|
||||
self.parentFolder = node.parentFolder()
|
||||
|
||||
if let webFeed = node.representedObject as? Feed {
|
||||
self.webFeed = webFeed
|
||||
if let feed = node.representedObject as? Feed {
|
||||
self.feed = feed
|
||||
self.folder = nil
|
||||
account = webFeed.account
|
||||
account = feed.account
|
||||
}
|
||||
else if let folder = node.representedObject as? Folder {
|
||||
self.webFeed = nil
|
||||
self.feed = nil
|
||||
self.folder = folder
|
||||
account = folder.account
|
||||
}
|
||||
@@ -144,7 +144,7 @@ private struct SidebarItemSpecifier {
|
||||
|
||||
func delete(completion: @escaping () -> Void) {
|
||||
|
||||
if let webFeed = webFeed {
|
||||
if let feed = feed {
|
||||
|
||||
guard let container = path.resolveContainer() else {
|
||||
completion()
|
||||
@@ -152,7 +152,7 @@ private struct SidebarItemSpecifier {
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
account?.removeWebFeed(webFeed, from: container) { result in
|
||||
account?.removeFeed(feed, from: container) { result in
|
||||
BatchUpdate.shared.end()
|
||||
completion()
|
||||
self.checkResult(result)
|
||||
@@ -172,22 +172,22 @@ private struct SidebarItemSpecifier {
|
||||
|
||||
func restore() {
|
||||
|
||||
if let _ = webFeed {
|
||||
restoreWebFeed()
|
||||
if let _ = feed {
|
||||
restoreFeed()
|
||||
}
|
||||
else if let _ = folder {
|
||||
restoreFolder()
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreWebFeed() {
|
||||
private func restoreFeed() {
|
||||
|
||||
guard let account = account, let feed = webFeed, let container = path.resolveContainer() else {
|
||||
guard let account = account, let feed = feed, let container = path.resolveContainer() else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
account.restoreWebFeed(feed, container: container) { result in
|
||||
account.restoreFeed(feed, container: container) { result in
|
||||
BatchUpdate.shared.end()
|
||||
self.checkResult(result)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// AddWebFeedDefaultContainer.swift
|
||||
// AddFeedDefaultContainer.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/16/19.
|
||||
|
||||
@@ -43,7 +43,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
|
||||
extension Article {
|
||||
|
||||
var feed: Feed? {
|
||||
return account?.existingWebFeed(withWebFeedID: webFeedID)
|
||||
return account?.existingFeed(withFeedID: feedID)
|
||||
}
|
||||
|
||||
var url: URL? {
|
||||
@@ -121,11 +121,11 @@ extension Article {
|
||||
return IconImageCache.shared.imageForArticle(self)
|
||||
}
|
||||
|
||||
func iconImageUrl(webFeed: Feed) -> URL? {
|
||||
func iconImageUrl(feed: Feed) -> URL? {
|
||||
if let image = iconImage() {
|
||||
let fm = FileManager.default
|
||||
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
let feedID = webFeed.feedID.replacingOccurrences(of: "/", with: "_")
|
||||
let feedID = feed.feedID.replacingOccurrences(of: "/", with: "_")
|
||||
#if os(macOS)
|
||||
path.appendPathComponent(feedID + "_smallIcon.tiff")
|
||||
#else
|
||||
@@ -193,7 +193,7 @@ extension Article {
|
||||
struct ArticlePathKey {
|
||||
static let accountID = "accountID"
|
||||
static let accountName = "accountName"
|
||||
static let webFeedID = "webFeedID"
|
||||
static let feedID = "feedID"
|
||||
static let articleID = "articleID"
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ extension Article {
|
||||
return [
|
||||
ArticlePathKey.accountID: accountID,
|
||||
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
|
||||
ArticlePathKey.webFeedID: webFeedID,
|
||||
ArticlePathKey.feedID: feedID,
|
||||
ArticlePathKey.articleID: articleID
|
||||
]
|
||||
}
|
||||
@@ -226,8 +226,8 @@ extension Article: SortableArticle {
|
||||
return articleID
|
||||
}
|
||||
|
||||
var sortableWebFeedID: String {
|
||||
return webFeedID
|
||||
var sortableFeedID: String {
|
||||
return feedID
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,18 +72,18 @@ final class FaviconDownloader {
|
||||
cache = [Feed: IconImage]()
|
||||
}
|
||||
|
||||
func favicon(for webFeed: Feed) -> IconImage? {
|
||||
func favicon(for feed: Feed) -> IconImage? {
|
||||
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
var homePageURL = webFeed.homePageURL
|
||||
if let faviconURL = webFeed.faviconURL {
|
||||
var homePageURL = feed.homePageURL
|
||||
if let faviconURL = feed.faviconURL {
|
||||
return favicon(with: faviconURL, homePageURL: homePageURL)
|
||||
}
|
||||
|
||||
if homePageURL == nil {
|
||||
// Base homePageURL off feedURL if needed. Won’t always be accurate, but is good enough.
|
||||
if let feedURL = URL(string: webFeed.url), let scheme = feedURL.scheme, let host = feedURL.host {
|
||||
if let feedURL = URL(string: feed.url), let scheme = feedURL.scheme, let host = feedURL.host {
|
||||
homePageURL = scheme + "://" + host + "/"
|
||||
}
|
||||
}
|
||||
@@ -94,16 +94,16 @@ final class FaviconDownloader {
|
||||
return nil
|
||||
}
|
||||
|
||||
func faviconAsIcon(for webFeed: Feed) -> IconImage? {
|
||||
func faviconAsIcon(for feed: Feed) -> IconImage? {
|
||||
|
||||
if let image = cache[webFeed] {
|
||||
if let image = cache[feed] {
|
||||
return image
|
||||
}
|
||||
|
||||
if let iconImage = favicon(for: webFeed), let imageData = iconImage.image.dataRepresentation() {
|
||||
if let iconImage = favicon(for: feed), let imageData = iconImage.image.dataRepresentation() {
|
||||
if let scaledImage = RSImage.scaledForIcon(imageData) {
|
||||
let scaledIconImage = IconImage(scaledImage)
|
||||
cache[webFeed] = scaledIconImage
|
||||
cache[feed] = scaledIconImage
|
||||
return scaledIconImage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,16 @@ final class FaviconGenerator {
|
||||
|
||||
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage
|
||||
|
||||
static func favicon(_ webFeed: Feed) -> IconImage {
|
||||
static func favicon(_ feed: Feed) -> IconImage {
|
||||
|
||||
if let favicon = FaviconGenerator.faviconGeneratorCache[webFeed.url] {
|
||||
if let favicon = FaviconGenerator.faviconGeneratorCache[feed.url] {
|
||||
return favicon
|
||||
}
|
||||
|
||||
let colorHash = ColorHash(webFeed.url)
|
||||
let colorHash = ColorHash(feed.url)
|
||||
if let favicon = AppAssets.faviconTemplateImage.maskWithColor(color: colorHash.color.cgColor) {
|
||||
let iconImage = IconImage(favicon, isBackgroundSupressed: true)
|
||||
FaviconGenerator.faviconGeneratorCache[webFeed.url] = iconImage
|
||||
FaviconGenerator.faviconGeneratorCache[feed.url] = iconImage
|
||||
return iconImage
|
||||
} else {
|
||||
return IconImage(AppAssets.faviconTemplateImage, isBackgroundSupressed: true)
|
||||
|
||||
@@ -15,7 +15,7 @@ class IconImageCache {
|
||||
static var shared = IconImageCache()
|
||||
|
||||
private var smartFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
private var webFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
private var feedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
private var faviconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
private var smallIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
private var authorIconImageCache = [Author: IconImage]()
|
||||
@@ -38,7 +38,7 @@ class IconImageCache {
|
||||
if let smartFeed = feed as? PseudoFeed {
|
||||
return imageForSmartFeed(smartFeed, feedID)
|
||||
}
|
||||
if let webFeed = feed as? Feed, let iconImage = imageForWebFeed(webFeed, feedID) {
|
||||
if let feed = feed as? Feed, let iconImage = imageForFeed(feed, feedID) {
|
||||
return iconImage
|
||||
}
|
||||
if let smallIconProvider = feed as? SmallIconProvider {
|
||||
@@ -60,7 +60,7 @@ class IconImageCache {
|
||||
|
||||
func emptyCache() {
|
||||
smartFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
webFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
feedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
faviconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
smallIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||
authorIconImageCache = [Author: IconImage]()
|
||||
@@ -80,18 +80,18 @@ private extension IconImageCache {
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageForWebFeed(_ webFeed: Feed, _ feedID: SidebarItemIdentifier) -> IconImage? {
|
||||
if let iconImage = webFeedIconImageCache[feedID] {
|
||||
func imageForFeed(_ feed: Feed, _ feedID: SidebarItemIdentifier) -> IconImage? {
|
||||
if let iconImage = feedIconImageCache[feedID] {
|
||||
return iconImage
|
||||
}
|
||||
if let iconImage = appDelegate.feedIconDownloader.icon(for: webFeed) {
|
||||
webFeedIconImageCache[feedID] = iconImage
|
||||
if let iconImage = appDelegate.feedIconDownloader.icon(for: feed) {
|
||||
feedIconImageCache[feedID] = iconImage
|
||||
return iconImage
|
||||
}
|
||||
if let faviconImage = faviconImageCache[feedID] {
|
||||
return faviconImage
|
||||
}
|
||||
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
||||
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
|
||||
faviconImageCache[feedID] = faviconImage
|
||||
return faviconImage
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ protocol SortableArticle {
|
||||
var sortableName: String { get }
|
||||
var sortableDate: Date { get }
|
||||
var sortableArticleID: String { get }
|
||||
var sortableWebFeedID: String { get }
|
||||
var sortableFeedID: String { get }
|
||||
}
|
||||
|
||||
struct ArticleSorter {
|
||||
@@ -34,7 +34,7 @@ struct ArticleSorter {
|
||||
sortByDateDirection: ComparisonResult) -> [T] {
|
||||
// Group articles by "feed-feedID" - feed ID is used to differentiate between
|
||||
// two feeds that have the same name
|
||||
let groupedArticles = Dictionary(grouping: articles) { "\($0.sortableName.lowercased())-\($0.sortableWebFeedID)" }
|
||||
let groupedArticles = Dictionary(grouping: articles) { "\($0.sortableName.lowercased())-\($0.sortableFeedID)" }
|
||||
return groupedArticles
|
||||
.sorted { $0.key < $1.key }
|
||||
.flatMap { (tuple) -> [T] in
|
||||
|
||||
@@ -66,9 +66,9 @@ private extension FeedTreeControllerDelegate {
|
||||
|
||||
var children = [AnyObject]()
|
||||
|
||||
for webFeed in container.topLevelFeeds {
|
||||
if let feedID = webFeed.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && webFeed.unreadCount == 0) {
|
||||
children.append(webFeed)
|
||||
for feed in container.topLevelFeeds {
|
||||
if let feedID = feed.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && feed.unreadCount == 0) {
|
||||
children.append(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +100,8 @@ private extension FeedTreeControllerDelegate {
|
||||
}
|
||||
|
||||
func createNode(representedObject: Any, parent: Node) -> Node? {
|
||||
if let webFeed = representedObject as? Feed {
|
||||
return createNode(webFeed: webFeed, parent: parent)
|
||||
if let feed = representedObject as? Feed {
|
||||
return createNode(feed: feed, parent: parent)
|
||||
}
|
||||
|
||||
if let folder = representedObject as? Folder {
|
||||
@@ -115,8 +115,8 @@ private extension FeedTreeControllerDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNode(webFeed: Feed, parent: Node) -> Node {
|
||||
return parent.createChildNode(webFeed)
|
||||
func createNode(feed: Feed, parent: Node) -> Node {
|
||||
return parent.createChildNode(feed)
|
||||
}
|
||||
|
||||
func createNode(folder: Folder, parent: Node) -> Node {
|
||||
|
||||
@@ -26,8 +26,8 @@ final class UserNotificationManager: NSObject {
|
||||
}
|
||||
|
||||
for article in articles {
|
||||
if !article.status.read, let webFeed = article.feed, webFeed.isNotifyAboutNewArticles ?? false {
|
||||
sendNotification(webFeed: webFeed, article: article)
|
||||
if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false {
|
||||
sendNotification(feed: feed, article: article)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,21 +53,21 @@ final class UserNotificationManager: NSObject {
|
||||
|
||||
private extension UserNotificationManager {
|
||||
|
||||
func sendNotification(webFeed: Feed, article: Article) {
|
||||
func sendNotification(feed: Feed, article: Article) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = webFeed.nameForDisplay
|
||||
content.title = feed.nameForDisplay
|
||||
if !ArticleStringFormatter.truncatedTitle(article).isEmpty {
|
||||
content.subtitle = ArticleStringFormatter.truncatedTitle(article)
|
||||
}
|
||||
content.body = ArticleStringFormatter.truncatedSummary(article)
|
||||
content.threadIdentifier = webFeed.feedID
|
||||
content.summaryArgument = "\(webFeed.nameForDisplay)"
|
||||
content.threadIdentifier = feed.feedID
|
||||
content.summaryArgument = "\(feed.nameForDisplay)"
|
||||
content.summaryArgumentCount = 1
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo]
|
||||
content.categoryIdentifier = "NEW_ARTICLE_NOTIFICATION_CATEGORY"
|
||||
if let attachment = thumbnailAttachment(for: article, webFeed: webFeed) {
|
||||
if let attachment = thumbnailAttachment(for: article, feed: feed) {
|
||||
content.attachments.append(attachment)
|
||||
}
|
||||
|
||||
@@ -78,12 +78,12 @@ private extension UserNotificationManager {
|
||||
/// Determine if there is an available icon for the article. This will then move it to the caches directory and make it avialble for the notification.
|
||||
/// - Parameters:
|
||||
/// - article: `Article`
|
||||
/// - webFeed: `WebFeed`
|
||||
/// - feed: `Feed`
|
||||
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
|
||||
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
|
||||
func thumbnailAttachment(for article: Article, webFeed: Feed) -> UNNotificationAttachment? {
|
||||
if let imageURL = article.iconImageUrl(webFeed: webFeed) {
|
||||
let thumbnail = try? UNNotificationAttachment(identifier: webFeed.feedID, url: imageURL, options: nil)
|
||||
func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? {
|
||||
if let imageURL = article.iconImageUrl(feed: feed) {
|
||||
let thumbnail = try? UNNotificationAttachment(identifier: feed.feedID, url: imageURL, options: nil)
|
||||
return thumbnail
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -19,10 +19,10 @@ class ArticleSorterTests: XCTestCase {
|
||||
func testSortByDateAscending() {
|
||||
let now = Date()
|
||||
|
||||
let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableWebFeedID: "4")
|
||||
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableWebFeedID: "6")
|
||||
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableWebFeedID: "6")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableWebFeedID: "5")
|
||||
let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableFeedID: "4")
|
||||
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableFeedID: "6")
|
||||
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableFeedID: "6")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableFeedID: "5")
|
||||
|
||||
let articles = [article1, article2, article3, article4]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
|
||||
@@ -40,11 +40,11 @@ class ArticleSorterTests: XCTestCase {
|
||||
let now = Date()
|
||||
|
||||
// Articles with the same date should end up being sorted by their article ID
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableWebFeedID: "3")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableWebFeedID: "4")
|
||||
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableWebFeedID: "5")
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "3")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableFeedID: "4")
|
||||
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableFeedID: "5")
|
||||
|
||||
let articles = [article1, article2, article3, article4, article5]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
|
||||
@@ -62,15 +62,15 @@ class ArticleSorterTests: XCTestCase {
|
||||
func testSortByDateAscendingWithGroupByFeed() {
|
||||
let now = Date()
|
||||
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableWebFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableWebFeedID: "2")
|
||||
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableWebFeedID: "3")
|
||||
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableWebFeedID: "3")
|
||||
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableWebFeedID: "2")
|
||||
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "1")
|
||||
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "0")
|
||||
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "0")
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "2")
|
||||
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableFeedID: "3")
|
||||
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "3")
|
||||
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableFeedID: "2")
|
||||
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
|
||||
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "0")
|
||||
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "0")
|
||||
|
||||
let articles = [article1, article2, article3, article4, article5, article6, article7, article8, article9]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles, sortDirection: .orderedAscending, groupByFeed: true)
|
||||
@@ -97,10 +97,10 @@ class ArticleSorterTests: XCTestCase {
|
||||
func testSortByDateDescending() {
|
||||
let now = Date()
|
||||
|
||||
let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableWebFeedID: "4")
|
||||
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableWebFeedID: "6")
|
||||
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableWebFeedID: "6")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableWebFeedID: "5")
|
||||
let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableFeedID: "4")
|
||||
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableFeedID: "6")
|
||||
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableFeedID: "6")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableFeedID: "5")
|
||||
|
||||
let articles = [article1, article2, article3, article4]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
|
||||
@@ -118,11 +118,11 @@ class ArticleSorterTests: XCTestCase {
|
||||
let now = Date()
|
||||
|
||||
// Articles with the same date should end up being sorted by their article ID
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableWebFeedID: "3")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableWebFeedID: "4")
|
||||
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableWebFeedID: "5")
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "3")
|
||||
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableFeedID: "4")
|
||||
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableFeedID: "5")
|
||||
|
||||
let articles = [article1, article2, article3, article4, article5]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
|
||||
@@ -140,15 +140,15 @@ class ArticleSorterTests: XCTestCase {
|
||||
func testSortByDateDescendingWithGroupByFeed() {
|
||||
let now = Date()
|
||||
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableWebFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableWebFeedID: "2")
|
||||
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableWebFeedID: "3")
|
||||
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableWebFeedID: "3")
|
||||
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableWebFeedID: "2")
|
||||
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "1")
|
||||
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "0")
|
||||
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "0")
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "2")
|
||||
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableFeedID: "3")
|
||||
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "3")
|
||||
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableFeedID: "2")
|
||||
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
|
||||
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "0")
|
||||
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "0")
|
||||
|
||||
let articles = [article1, article2, article3, article4, article5, article6, article7, article8, article9]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles, sortDirection: .orderedDescending, groupByFeed: true)
|
||||
@@ -175,11 +175,11 @@ class ArticleSorterTests: XCTestCase {
|
||||
func testGroupByFeedWithCaseInsensitiveFeedNames() {
|
||||
let now = Date()
|
||||
|
||||
let article1 = TestArticle(sortableName: "phil's feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "PhIl's FEed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "1")
|
||||
let article3 = TestArticle(sortableName: "APPLE's feed", sortableDate: now, sortableArticleID: "3", sortableWebFeedID: "2")
|
||||
let article4 = TestArticle(sortableName: "PHIL'S FEED", sortableDate: now, sortableArticleID: "4", sortableWebFeedID: "1")
|
||||
let article5 = TestArticle(sortableName: "apple's feed", sortableDate: now, sortableArticleID: "5", sortableWebFeedID: "2")
|
||||
let article1 = TestArticle(sortableName: "phil's feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
|
||||
let article2 = TestArticle(sortableName: "PhIl's FEed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
|
||||
let article3 = TestArticle(sortableName: "APPLE's feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "2")
|
||||
let article4 = TestArticle(sortableName: "PHIL'S FEED", sortableDate: now, sortableArticleID: "4", sortableFeedID: "1")
|
||||
let article5 = TestArticle(sortableName: "apple's feed", sortableDate: now, sortableArticleID: "5", sortableFeedID: "2")
|
||||
|
||||
let articles = [article1, article2, article3, article4, article5]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
|
||||
@@ -201,11 +201,11 @@ class ArticleSorterTests: XCTestCase {
|
||||
let now = Date()
|
||||
|
||||
// Articles with the same feed name should be sorted by feed ID
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableWebFeedID: "2")
|
||||
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableWebFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "3", sortableWebFeedID: "1")
|
||||
let article4 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "4", sortableWebFeedID: "2")
|
||||
let article5 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "5", sortableWebFeedID: "1")
|
||||
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
|
||||
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
|
||||
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "1")
|
||||
let article4 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "4", sortableFeedID: "2")
|
||||
let article5 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "5", sortableFeedID: "1")
|
||||
|
||||
let articles = [article1, article2, article3, article4, article5]
|
||||
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
|
||||
@@ -226,7 +226,7 @@ private struct TestArticle: SortableArticle, Equatable {
|
||||
let sortableName: String
|
||||
let sortableDate: Date
|
||||
let sortableArticleID: String
|
||||
let sortableWebFeedID: String
|
||||
let sortableFeedID: String
|
||||
}
|
||||
|
||||
private extension Array where Element == TestArticle {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- this script just tests that no error was generated from the script
|
||||
try
|
||||
tell application "NetNewsWire"
|
||||
exists webFeed 1 of account 1
|
||||
exists feed 1 of account 1
|
||||
end tell
|
||||
on error message
|
||||
return {test_result:false, script_result:message}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- this script just tests that no error was generated from the script
|
||||
try
|
||||
tell application "NetNewsWire"
|
||||
opml representation of webFeed 1 of account 1
|
||||
opml representation of feed 1 of account 1
|
||||
end tell
|
||||
on error message
|
||||
return {test_result:false, script_result:message}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- this script just tests that no error was generated from the script
|
||||
try
|
||||
tell application "NetNewsWire"
|
||||
{name, url} of every webFeed of every account
|
||||
{name, url} of every feed of every account
|
||||
end tell
|
||||
on error message
|
||||
return {test_result:false, script_result:message}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- and that the returned list is greater than 0
|
||||
try
|
||||
tell application "NetNewsWire"
|
||||
set namesResult to name of every author of every webFeed of every account
|
||||
set namesResult to name of every author of every feed of every account
|
||||
end tell
|
||||
set test_result to ((count items of namesResult) > 0)
|
||||
on error message
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- this script just tests that no error was generated from the script
|
||||
try
|
||||
tell application "NetNewsWire"
|
||||
title of every article of webFeed "Six Colors" where read is true
|
||||
title of every article of feed "Six Colors" where read is true
|
||||
end tell
|
||||
on error message
|
||||
return {test_result:false, script_result:message}
|
||||
|
||||
@@ -42,7 +42,7 @@ class SharingTests: XCTestCase {
|
||||
let articleId = randomId()
|
||||
return Article(accountID: randomId(),
|
||||
articleID: articleId,
|
||||
webFeedID: randomId(),
|
||||
feedID: randomId(),
|
||||
uniqueID: randomId(),
|
||||
title: title,
|
||||
contentHTML: nil,
|
||||
|
||||
@@ -147,7 +147,7 @@ class AddFeedViewController: UITableViewController {
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if indexPath.row == 2 {
|
||||
let navController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddWebFeedFolderNavViewController") as! UINavigationController
|
||||
let navController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedFolderNavViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let folderViewController = navController.topViewController as! AddFeedFolderViewController
|
||||
folderViewController.delegate = self
|
||||
@@ -159,7 +159,7 @@ class AddFeedViewController: UITableViewController {
|
||||
|
||||
}
|
||||
|
||||
// MARK: AddWebFeedFolderViewControllerDelegate
|
||||
// MARK: AddFeedFolderViewControllerDelegate
|
||||
|
||||
extension AddFeedViewController: AddFeedFolderViewControllerDelegate {
|
||||
func didSelect(container: Container) {
|
||||
|
||||
@@ -68,7 +68,7 @@ class WebViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil)
|
||||
@@ -83,7 +83,7 @@ class WebViewController: UIViewController {
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
@@ -451,8 +451,8 @@ extension WebViewController: WKScriptMessageHandler {
|
||||
case MessageName.imageWasClicked:
|
||||
imageWasClicked(body: message.body as? String)
|
||||
case MessageName.showFeedInspector:
|
||||
if let webFeed = article?.feed {
|
||||
coordinator.showFeedInspector(for: webFeed)
|
||||
if let feed = article?.feed {
|
||||
coordinator.showFeedInspector(for: feed)
|
||||
}
|
||||
default:
|
||||
return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// WebFeedInspectorViewController.swift
|
||||
// FeedInspectorViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/6/19.
|
||||
@@ -11,11 +11,11 @@ import Account
|
||||
import SafariServices
|
||||
import UserNotifications
|
||||
|
||||
class WebFeedInspectorViewController: UITableViewController {
|
||||
class FeedInspectorViewController: UITableViewController {
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
|
||||
|
||||
var webFeed: Feed!
|
||||
var feed: Feed!
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
@IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch!
|
||||
@IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch!
|
||||
@@ -24,13 +24,13 @@ class WebFeedInspectorViewController: UITableViewController {
|
||||
|
||||
private var headerView: InspectorIconHeaderView?
|
||||
private var iconImage: IconImage? {
|
||||
return IconImageCache.shared.imageForFeed(webFeed)
|
||||
return IconImageCache.shared.imageForFeed(feed)
|
||||
}
|
||||
|
||||
private let homePageIndexPath = IndexPath(row: 0, section: 1)
|
||||
|
||||
private var shouldHideHomePageSection: Bool {
|
||||
return webFeed.homePageURL == nil
|
||||
return feed.homePageURL == nil
|
||||
}
|
||||
|
||||
private var userNotificationSettings: UNNotificationSettings?
|
||||
@@ -38,18 +38,18 @@ class WebFeedInspectorViewController: UITableViewController {
|
||||
override func viewDidLoad() {
|
||||
tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
|
||||
navigationItem.title = webFeed.nameForDisplay
|
||||
nameTextField.text = webFeed.nameForDisplay
|
||||
navigationItem.title = feed.nameForDisplay
|
||||
nameTextField.text = feed.nameForDisplay
|
||||
|
||||
notifyAboutNewArticlesSwitch.setOn(webFeed.isNotifyAboutNewArticles ?? false, animated: false)
|
||||
notifyAboutNewArticlesSwitch.setOn(feed.isNotifyAboutNewArticles ?? false, animated: false)
|
||||
|
||||
alwaysShowReaderViewSwitch.setOn(webFeed.isArticleExtractorAlwaysOn ?? false, animated: false)
|
||||
alwaysShowReaderViewSwitch.setOn(feed.isArticleExtractorAlwaysOn ?? false, animated: false)
|
||||
|
||||
homePageLabel.text = webFeed.homePageURL?.decodedURLString
|
||||
feedURLLabel.text = webFeed.url.decodedURLString
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
homePageLabel.text = feed.homePageURL?.decodedURLString
|
||||
feedURLLabel.text = feed.url.decodedURLString
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
}
|
||||
@@ -59,15 +59,15 @@ class WebFeedInspectorViewController: UITableViewController {
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
if nameTextField.text != webFeed.nameForDisplay {
|
||||
if nameTextField.text != feed.nameForDisplay {
|
||||
let nameText = nameTextField.text ?? ""
|
||||
let newName = nameText.isEmpty ? (webFeed.name ?? NSLocalizedString("Untitled", comment: "Feed name")) : nameText
|
||||
webFeed.rename(to: newName) { _ in }
|
||||
let newName = nameText.isEmpty ? (feed.name ?? NSLocalizedString("Untitled", comment: "Feed name")) : nameText
|
||||
feed.rename(to: newName) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
@objc func webFeedIconDidBecomeAvailable(_ notification: Notification) {
|
||||
@objc func feedIconDidBecomeAvailable(_ notification: Notification) {
|
||||
headerView?.iconView.iconImage = iconImage
|
||||
}
|
||||
|
||||
@@ -80,13 +80,13 @@ class WebFeedInspectorViewController: UITableViewController {
|
||||
notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn
|
||||
present(notificationUpdateErrorAlert(), animated: true, completion: nil)
|
||||
} else if settings.authorizationStatus == .authorized {
|
||||
webFeed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn
|
||||
feed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn
|
||||
} else {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in
|
||||
self.updateNotificationSettings()
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
self.webFeed.isNotifyAboutNewArticles = self.notifyAboutNewArticlesSwitch.isOn
|
||||
self.feed.isNotifyAboutNewArticles = self.notifyAboutNewArticlesSwitch.isOn
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
} else {
|
||||
@@ -99,7 +99,7 @@ class WebFeedInspectorViewController: UITableViewController {
|
||||
}
|
||||
|
||||
@IBAction func alwaysShowReaderViewChanged(_ sender: Any) {
|
||||
webFeed.isArticleExtractorAlwaysOn = alwaysShowReaderViewSwitch.isOn
|
||||
feed.isArticleExtractorAlwaysOn = alwaysShowReaderViewSwitch.isOn
|
||||
}
|
||||
|
||||
@IBAction func done(_ sender: Any) {
|
||||
@@ -128,7 +128,7 @@ class WebFeedInspectorViewController: UITableViewController {
|
||||
|
||||
// MARK: Table View
|
||||
|
||||
extension WebFeedInspectorViewController {
|
||||
extension FeedInspectorViewController {
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
let numberOfSections = super.numberOfSections(in: tableView)
|
||||
@@ -150,7 +150,7 @@ extension WebFeedInspectorViewController {
|
||||
return cell
|
||||
}
|
||||
label.numberOfLines = 2
|
||||
label.text = webFeed.notificationDisplayName.capitalized
|
||||
label.text = feed.notificationDisplayName.capitalized
|
||||
}
|
||||
return cell
|
||||
}
|
||||
@@ -171,7 +171,7 @@ extension WebFeedInspectorViewController {
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if shift(indexPath) == homePageIndexPath,
|
||||
let homePageUrlString = webFeed.homePageURL,
|
||||
let homePageUrlString = feed.homePageURL,
|
||||
let homePageUrl = URL(string: homePageUrlString) {
|
||||
|
||||
let safari = SFSafariViewController(url: homePageUrl)
|
||||
@@ -186,7 +186,7 @@ extension WebFeedInspectorViewController {
|
||||
|
||||
// MARK: UITextFieldDelegate
|
||||
|
||||
extension WebFeedInspectorViewController: UITextFieldDelegate {
|
||||
extension FeedInspectorViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
@@ -197,7 +197,7 @@ extension WebFeedInspectorViewController: UITextFieldDelegate {
|
||||
|
||||
// MARK: UNUserNotificationCenter
|
||||
|
||||
extension WebFeedInspectorViewController {
|
||||
extension FeedInspectorViewController {
|
||||
|
||||
@objc
|
||||
func updateNotificationSettings() {
|
||||
@@ -13,11 +13,11 @@ import Account
|
||||
extension MasterFeedViewController: UITableViewDragDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let node = coordinator.nodeFor(indexPath), let webFeed = node.representedObject as? Feed else {
|
||||
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed else {
|
||||
return [UIDragItem]()
|
||||
}
|
||||
|
||||
let data = webFeed.url.data(using: .utf8)
|
||||
let data = feed.url.data(using: .utf8)
|
||||
let itemProvider = NSItemProvider()
|
||||
|
||||
itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .ownProcess) { completion in
|
||||
|
||||
@@ -31,8 +31,8 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
// Validate account specific behaviors...
|
||||
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
||||
let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
|
||||
let sourceWebFeed = sourceNode.representedObject as? Feed,
|
||||
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasFeed(withURL: sourceWebFeed.url) {
|
||||
let sourceFeed = sourceNode.representedObject as? Feed,
|
||||
sourceFeed.account?.accountID != destAccount.accountID && destAccount.hasFeed(withURL: sourceFeed.url) {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
@@ -91,16 +91,16 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
}
|
||||
}()
|
||||
|
||||
guard let destination = destinationContainer, let webFeed = dragNode.representedObject as? Feed else { return }
|
||||
guard let destination = destinationContainer, let feed = dragNode.representedObject as? Feed else { return }
|
||||
|
||||
if source.account == destination.account {
|
||||
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||
moveFeedInAccount(feed: feed, sourceContainer: source, destinationContainer: destination)
|
||||
} else {
|
||||
moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||
moveFeedBetweenAccounts(feed: feed, sourceContainer: source, destinationContainer: destination)
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
func moveFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
guard sourceContainer !== destinationContainer else { return }
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
@@ -115,7 +115,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
func moveFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
|
||||
if let existingFeed = destinationContainer.account?.existingFeed(withURL: feed.url) {
|
||||
|
||||
@@ -123,7 +123,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
destinationContainer.account?.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
|
||||
sourceContainer.account?.removeFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -144,7 +144,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
destinationContainer.account?.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
|
||||
sourceContainer.account?.removeFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
@@ -68,8 +68,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
@@ -129,19 +129,19 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
applyToAvailableCells(configureIcon)
|
||||
}
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let webFeed = note.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
return
|
||||
}
|
||||
applyToCellsForRepresentedObject(webFeed, configureIcon(_:_:))
|
||||
applyToCellsForRepresentedObject(feed, configureIcon(_:_:))
|
||||
}
|
||||
|
||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||
guard let webFeed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
|
||||
@objc func feedSettingDidChange(_ note: Notification) {
|
||||
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
|
||||
return
|
||||
}
|
||||
if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL {
|
||||
configureCellsForRepresentedObject(webFeed)
|
||||
configureCellsForRepresentedObject(feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,13 +268,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
renameAction.backgroundColor = UIColor.systemOrange
|
||||
actions.append(renameAction)
|
||||
|
||||
if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed {
|
||||
if let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed {
|
||||
let moreTitle = NSLocalizedString("More", comment: "More")
|
||||
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
|
||||
|
||||
if let self = self {
|
||||
|
||||
let alert = UIAlertController(title: webFeed.nameForDisplay, message: nil, preferredStyle: .actionSheet)
|
||||
let alert = UIAlertController(title: feed.nameForDisplay, message: nil, preferredStyle: .actionSheet)
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1)
|
||||
@@ -324,7 +324,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return nil
|
||||
}
|
||||
if feed is Feed {
|
||||
return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
||||
return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
||||
} else if feed is Folder {
|
||||
return makeFolderContextMenu(indexPath: indexPath)
|
||||
} else if feed is PseudoFeed {
|
||||
@@ -439,9 +439,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
|
||||
|
||||
let addWebFeedActionTitle = NSLocalizedString("Add Web Feed", comment: "Add Web Feed")
|
||||
let addWebFeedAction = UIAlertAction(title: addWebFeedActionTitle, style: .default) { _ in
|
||||
self.coordinator.showAddWebFeed()
|
||||
let addFeedActionTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
|
||||
let addFeedAction = UIAlertAction(title: addFeedActionTitle, style: .default) { _ in
|
||||
self.coordinator.showAddFeed()
|
||||
}
|
||||
|
||||
let addWebFolderdActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
|
||||
@@ -449,7 +449,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
self.coordinator.showAddFolder()
|
||||
}
|
||||
|
||||
alertController.addAction(addWebFeedAction)
|
||||
alertController.addAction(addFeedAction)
|
||||
|
||||
alertController.addAction(addWebFolderAction)
|
||||
alertController.addAction(cancelAction)
|
||||
@@ -649,11 +649,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
var menuItems: [UIAction] = []
|
||||
|
||||
let addWebFeedActionTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
|
||||
let addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.plus) { _ in
|
||||
self.coordinator.showAddWebFeed()
|
||||
let addFeedActionTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
|
||||
let addFeedAction = UIAction(title: addFeedActionTitle, image: AppAssets.plus) { _ in
|
||||
self.coordinator.showAddFeed()
|
||||
}
|
||||
menuItems.append(addWebFeedAction)
|
||||
menuItems.append(addFeedAction)
|
||||
|
||||
let addWebFolderActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
|
||||
let addWebFolderAction = UIAction(title: addWebFolderActionTitle, image: AppAssets.folderOutlinePlus) { _ in
|
||||
@@ -896,7 +896,7 @@ private extension MasterFeedViewController {
|
||||
coordinator.collapse(node)
|
||||
}
|
||||
|
||||
func makeWebFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
@@ -1000,8 +1000,8 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let url = URL(string: feed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1013,8 +1013,8 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let url = URL(string: feed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1027,8 +1027,8 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let homePageURL = feed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
@@ -1041,8 +1041,8 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let homePageURL = feed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
@@ -1056,14 +1056,14 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
webFeed.unreadCount > 0,
|
||||
let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
feed.unreadCount > 0,
|
||||
let articles = try? feed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
@@ -1096,13 +1096,13 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func getInfoAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Get Info", comment: "Get Info")
|
||||
let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in
|
||||
self?.coordinator.showFeedInspector(for: webFeed)
|
||||
self?.coordinator.showFeedInspector(for: feed)
|
||||
}
|
||||
return action
|
||||
}
|
||||
@@ -1124,13 +1124,13 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Get Info", comment: "Get Info")
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
self?.coordinator.showFeedInspector(for: webFeed)
|
||||
self?.coordinator.showFeedInspector(for: feed)
|
||||
completion(true)
|
||||
}
|
||||
return action
|
||||
@@ -1196,8 +1196,8 @@ private extension MasterFeedViewController {
|
||||
return
|
||||
}
|
||||
|
||||
if let webFeed = feed as? Feed {
|
||||
webFeed.rename(to: name) { result in
|
||||
if let feed = feed as? Feed {
|
||||
feed.rename(to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
|
||||
@@ -52,7 +52,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
@@ -447,7 +447,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
}
|
||||
}
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
|
||||
if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
|
||||
titleView.iconView.iconImage = coordinator.timelineIconImage
|
||||
@@ -548,7 +548,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
|
||||
let prototypeID = "prototype"
|
||||
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date())
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||
|
||||
let prototypeCellData = MasterTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, featuredImage: nil, numberOfLines: numberOfTextLines, iconSize: iconSize)
|
||||
|
||||
@@ -846,31 +846,31 @@ private extension MasterTimelineViewController {
|
||||
}
|
||||
|
||||
func discloseFeedAction(_ article: Article) -> UIAction? {
|
||||
guard let webFeed = article.feed,
|
||||
!coordinator.timelineFeedIsEqualTo(webFeed) else { return nil }
|
||||
guard let feed = article.feed,
|
||||
!coordinator.timelineFeedIsEqualTo(feed) else { return nil }
|
||||
|
||||
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
|
||||
let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in
|
||||
self?.coordinator.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
|
||||
self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation])
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func discloseFeedAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let webFeed = article.feed,
|
||||
!coordinator.timelineFeedIsEqualTo(webFeed) else { return nil }
|
||||
guard let feed = article.feed,
|
||||
!coordinator.timelineFeedIsEqualTo(feed) else { return nil }
|
||||
|
||||
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
self?.coordinator.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
|
||||
self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation])
|
||||
completion(true)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
||||
guard let webFeed = article.feed else { return nil }
|
||||
guard let fetchedArticles = try? webFeed.fetchArticles() else {
|
||||
guard let feed = article.feed else { return nil }
|
||||
guard let fetchedArticles = try? feed.fetchArticles() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -881,7 +881,7 @@ private extension MasterTimelineViewController {
|
||||
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
||||
@@ -892,8 +892,8 @@ private extension MasterTimelineViewController {
|
||||
}
|
||||
|
||||
func markAllInFeedAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let webFeed = article.feed else { return nil }
|
||||
guard let fetchedArticles = try? webFeed.fetchArticles() else {
|
||||
guard let feed = article.feed else { return nil }
|
||||
guard let fetchedArticles = try? feed.fetchArticles() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -903,7 +903,7 @@ private extension MasterTimelineViewController {
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class RootSplitViewController: UISplitViewController {
|
||||
}
|
||||
|
||||
@objc func addNewFeed(_ sender: Any?) {
|
||||
coordinator.showAddWebFeed()
|
||||
coordinator.showAddFeed()
|
||||
}
|
||||
|
||||
@objc func addNewFolder(_ sender: Any?) {
|
||||
|
||||
@@ -397,7 +397,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
case .readArticle:
|
||||
self.handleReadArticle(activity.userInfo)
|
||||
case .addFeedIntent:
|
||||
self.showAddWebFeed()
|
||||
self.showAddFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,10 +545,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
@objc func userDidAddFeed(_ notification: Notification) {
|
||||
guard let webFeed = notification.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
guard let feed = notification.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
return
|
||||
}
|
||||
discloseWebFeed(webFeed, animations: [.scroll, .navigation])
|
||||
discloseFeed(feed, animations: [.scroll, .navigation])
|
||||
}
|
||||
|
||||
@objc func userDefaultsDidChange(_ note: Notification) {
|
||||
@@ -557,7 +557,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<Feed> else {
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1128,25 +1128,25 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
return timelineFeed == feed
|
||||
}
|
||||
|
||||
func discloseWebFeed(_ webFeed: Feed, initialLoad: Bool = false, animations: Animations = [], completion: (() -> Void)? = nil) {
|
||||
func discloseFeed(_ feed: Feed, initialLoad: Bool = false, animations: Animations = [], completion: (() -> Void)? = nil) {
|
||||
if isSearching {
|
||||
masterTimelineViewController?.hideSearch()
|
||||
}
|
||||
|
||||
guard let account = webFeed.account else {
|
||||
guard let account = feed.account else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
|
||||
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(feed) })
|
||||
|
||||
markExpanded(account)
|
||||
if let parentFolder = parentFolder {
|
||||
markExpanded(parentFolder)
|
||||
}
|
||||
|
||||
if let webFeedFeedID = webFeed.sidebarItemID {
|
||||
self.treeControllerDelegate.addFilterException(webFeedFeedID)
|
||||
if let sidebarItemID = feed.sidebarItemID {
|
||||
self.treeControllerDelegate.addFilterException(sidebarItemID)
|
||||
}
|
||||
if let parentFolderFeedID = parentFolder?.sidebarItemID {
|
||||
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
||||
@@ -1155,7 +1155,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
rebuildBackingStores(initialLoad: initialLoad, completion: {
|
||||
self.treeControllerDelegate.resetFilterExceptions()
|
||||
self.selectFeed(nil) {
|
||||
self.selectFeed(webFeed, animations: animations, completion: completion)
|
||||
self.selectFeed(feed, animations: animations, completion: completion)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1196,9 +1196,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
func showFeedInspector() {
|
||||
let timelineWebFeed = timelineFeed as? Feed
|
||||
let timelineFeed = timelineFeed as? Feed
|
||||
let articleFeed = currentArticle?.feed
|
||||
guard let feed = timelineWebFeed ?? articleFeed else {
|
||||
guard let feed = timelineFeed ?? articleFeed else {
|
||||
return
|
||||
}
|
||||
showFeedInspector(for: feed)
|
||||
@@ -1207,19 +1207,19 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
func showFeedInspector(for feed: Feed) {
|
||||
let feedInspectorNavController =
|
||||
UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController
|
||||
let feedInspectorController = feedInspectorNavController.topViewController as! WebFeedInspectorViewController
|
||||
let feedInspectorController = feedInspectorNavController.topViewController as! FeedInspectorViewController
|
||||
feedInspectorNavController.modalPresentationStyle = .formSheet
|
||||
feedInspectorNavController.preferredContentSize = WebFeedInspectorViewController.preferredContentSizeForFormSheetDisplay
|
||||
feedInspectorController.webFeed = feed
|
||||
feedInspectorNavController.preferredContentSize = FeedInspectorViewController.preferredContentSizeForFormSheetDisplay
|
||||
feedInspectorController.feed = feed
|
||||
rootSplitViewController.present(feedInspectorNavController, animated: true)
|
||||
}
|
||||
|
||||
func showAddWebFeed(initialFeed: String? = nil, initialFeedName: String? = nil) {
|
||||
func showAddFeed(initialFeed: String? = nil, initialFeedName: String? = nil) {
|
||||
|
||||
// Since Add Feed can be opened from anywhere with a keyboard shortcut, we have to deselect any currently selected feeds
|
||||
selectFeed(nil)
|
||||
|
||||
let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddWebFeedViewControllerNav") as! UINavigationController
|
||||
let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedViewControllerNav") as! UINavigationController
|
||||
|
||||
let addViewController = addNavViewController.topViewController as! AddFeedViewController
|
||||
addViewController.initialFeed = initialFeed
|
||||
@@ -1476,10 +1476,10 @@ private extension SceneCoordinator {
|
||||
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
}
|
||||
} else if let webFeed = feed as? Feed {
|
||||
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.feedID) != nil {
|
||||
} else if let feed = feed as? Feed {
|
||||
if feed.account?.existingFeed(withFeedID: feed.feedID) != nil {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
addParentFolderToFilterExceptions(webFeed)
|
||||
addParentFolderToFilterExceptions(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2002,7 +2002,7 @@ private extension SceneCoordinator {
|
||||
if !unsortedArticleIDs.contains(article.articleID) {
|
||||
updatedArticles.insert(article)
|
||||
}
|
||||
if article.account?.existingWebFeed(withWebFeedID: article.webFeedID) == nil {
|
||||
if article.account?.existingFeed(withFeedID: article.feedID) == nil {
|
||||
updatedArticles.remove(article)
|
||||
}
|
||||
}
|
||||
@@ -2086,7 +2086,7 @@ private extension SceneCoordinator {
|
||||
}
|
||||
} else if let folder = timelineFeed as? Folder {
|
||||
for oneFeed in feeds {
|
||||
if folder.hasWebFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -2284,14 +2284,14 @@ private extension SceneCoordinator {
|
||||
}
|
||||
})
|
||||
|
||||
case .webFeed(let accountID, let webFeedID):
|
||||
case .feed(let accountID, let feedID):
|
||||
guard let accountNode = findAccountNode(accountID: accountID),
|
||||
let account = accountNode.representedObject as? Account,
|
||||
let webFeed = account.existingWebFeed(withWebFeedID: webFeedID) else {
|
||||
let feed = account.existingFeed(withFeedID: feedID) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.discloseWebFeed(webFeed, initialLoad: true) {
|
||||
self.discloseFeed(feed, initialLoad: true) {
|
||||
self.masterFeedViewController.focus()
|
||||
}
|
||||
}
|
||||
@@ -2303,7 +2303,7 @@ private extension SceneCoordinator {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let accountName = articlePathUserInfo[ArticlePathKey.accountName] as? String,
|
||||
let webFeedID = articlePathUserInfo[ArticlePathKey.webFeedID] as? String,
|
||||
let feedID = articlePathUserInfo[ArticlePathKey.feedID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String,
|
||||
let accountNode = findAccountNode(accountID: accountID, accountName: accountName),
|
||||
let account = accountNode.representedObject as? Account else {
|
||||
@@ -2312,20 +2312,20 @@ private extension SceneCoordinator {
|
||||
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||
|
||||
if restoreFeedSelection(userInfo, accountID: accountID, webFeedID: webFeedID, articleID: articleID) {
|
||||
if restoreFeedSelection(userInfo, accountID: accountID, feedID: feedID, articleID: articleID) {
|
||||
return
|
||||
}
|
||||
|
||||
guard let webFeed = account.existingWebFeed(withWebFeedID: webFeedID) else {
|
||||
guard let feed = account.existingFeed(withFeedID: feedID) else {
|
||||
return
|
||||
}
|
||||
|
||||
discloseWebFeed(webFeed) {
|
||||
discloseFeed(feed) {
|
||||
self.selectArticleInCurrentFeed(articleID)
|
||||
}
|
||||
}
|
||||
|
||||
func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
|
||||
func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, feedID: String, articleID: String) -> Bool {
|
||||
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
|
||||
let feedIdentifier = SidebarItemIdentifier(userInfo: feedIdentifierUserInfo),
|
||||
let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool,
|
||||
@@ -2345,11 +2345,11 @@ private extension SceneCoordinator {
|
||||
}
|
||||
return found
|
||||
|
||||
case .webFeed:
|
||||
case .feed:
|
||||
let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
|
||||
if found {
|
||||
treeControllerDelegate.addFilterException(feedIdentifier)
|
||||
if let webFeedNode = nodeFor(feedID: feedIdentifier), let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.sidebarItemID {
|
||||
if let feedNode = nodeFor(feedID: feedIdentifier), let folder = feedNode.parent?.representedObject as? Folder, let folderFeedID = folder.sidebarItemID {
|
||||
treeControllerDelegate.addFilterException(folderFeedID)
|
||||
}
|
||||
}
|
||||
@@ -2378,8 +2378,8 @@ private extension SceneCoordinator {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findWebFeedNode(webFeedID: String, beginningAt startingNode: Node) -> Node? {
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == webFeedID }) {
|
||||
func findFeedNode(feedID: String, beginningAt startingNode: Node) -> Node? {
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -112,7 +112,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
if urlString.starts(with: "feed:") || urlString.starts(with: "feeds:") {
|
||||
let normalizedURLString = urlString.normalizedURL
|
||||
if normalizedURLString.mayBeURL {
|
||||
self.coordinator.showAddWebFeed(initialFeed: normalizedURLString, initialFeedName: nil)
|
||||
self.coordinator.showAddFeed(initialFeed: normalizedURLString, initialFeedName: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ private extension SceneDelegate {
|
||||
case "com.ranchero.NetNewsWire.ShowSearch":
|
||||
coordinator.showSearch()
|
||||
case "com.ranchero.NetNewsWire.ShowAdd":
|
||||
coordinator.showAddWebFeed()
|
||||
coordinator.showAddFeed()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ private extension SettingsViewController {
|
||||
func addFeed() {
|
||||
self.dismiss(animated: true)
|
||||
|
||||
let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddWebFeedViewControllerNav") as! UINavigationController
|
||||
let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedViewControllerNav") as! UINavigationController
|
||||
let addViewController = addNavViewController.topViewController as! AddFeedViewController
|
||||
addViewController.initialFeed = AccountManager.netNewsWireNewsURL
|
||||
addViewController.initialFeedName = NSLocalizedString("NetNewsWire News", comment: "NetNewsWire News")
|
||||
|
||||
@@ -67,7 +67,7 @@ private extension TimelinePreviewTableViewController {
|
||||
|
||||
let prototypeID = "prototype"
|
||||
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date())
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||
|
||||
let iconImage = IconImage(AppAssets.faviconTemplateImage.withTintColor(AppAssets.secondaryAccentColor))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user