// // ArticleUtilities.swift // NetNewsWire // // Created by Brent Simmons on 7/25/15. // Copyright © 2015 Ranchero Software, LLC. All rights reserved. // import Foundation import Articles import Account import Images // These handle multiple accounts. @MainActor func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) { let d: [String: Set
] = accountAndArticlesDictionary(articles) let group = DispatchGroup() for (accountID, accountArticles) in d { guard let account = AccountManager.shared.existingAccount(with: accountID) else { continue } group.enter() Task { @MainActor in try? await account.markArticles(accountArticles, statusKey: statusKey, flag: flag) group.leave() } } group.notify(queue: .main) { completion?() } } private func accountAndArticlesDictionary(_ articles: Set
) -> [String: Set
] { let d = Dictionary(grouping: articles, by: { $0.accountID }) return d.mapValues{ Set($0) } } extension Article { @MainActor var feed: Feed? { return account?.existingFeed(withFeedID: feedID) } var url: URL? { return URL.repairingIfRequired(rawLink) } var externalURL: URL? { return URL.repairingIfRequired(rawExternalLink) } var imageURL: URL? { return URL.repairingIfRequired(rawImageLink) } var link: String? { // Prefer link from URL, if one can be created, as these are repaired if required. // Provide the raw link if URL creation fails. return url?.absoluteString ?? rawLink } var externalLink: String? { // Prefer link from externalURL, if one can be created, as these are repaired if required. // Provide the raw link if URL creation fails. return externalURL?.absoluteString ?? rawExternalLink } var imageLink: String? { // Prefer link from imageURL, if one can be created, as these are repaired if required. // Provide the raw link if URL creation fails. return imageURL?.absoluteString ?? rawImageLink } var preferredLink: String? { if let link = link, !link.isEmpty { return link } if let externalLink = externalLink, !externalLink.isEmpty { return externalLink } return nil } var preferredURL: URL? { return url ?? externalURL } var body: String? { return contentHTML ?? contentText ?? summary } var logicalDatePublished: Date { return datePublished ?? dateModified ?? status.dateArrived } @MainActor var isAvailableToMarkUnread: Bool { guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in switch behavior { case .disallowMarkAsUnreadAfterPeriod(let days): return days default: return nil } }).first else { return true } if logicalDatePublished.byAdding(days: markUnreadWindow) > Date() { return true } else { return false } } @MainActor func iconImage() -> IconImage? { return IconImageCache.shared.imageForArticle(self) } @MainActor func iconImageUrl(feed: Feed) -> URL? { if let image = iconImage() { let fm = FileManager.default var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0] let feedID = feed.feedID.replacingOccurrences(of: "/", with: "_") #if os(macOS) path.appendPathComponent(feedID + "_smallIcon.tiff") #else path.appendPathComponent(feedID + "_smallIcon.png") #endif fm.createFile(atPath: path.path, contents: image.image.dataRepresentation()!, attributes: nil) return path } else { return nil } } @MainActor func byline() -> String { guard let authors = authors ?? feed?.authors, !authors.isEmpty else { return "" } // If the author's name is the same as the feed, then we don't want to display it. // This code assumes that multiple authors would never match the feed name so that // if there feed owner has an article co-author all authors are given the byline. if authors.count == 1, let author = authors.first { if author.name == feed?.nameForDisplay { return "" } } var byline = "" var isFirstAuthor = true for author in authors { if !isFirstAuthor { byline += ", " } isFirstAuthor = false var authorEmailAddress: String? = nil if let emailAddress = author.emailAddress, !(emailAddress.contains("noreply@") || emailAddress.contains("no-reply@")) { authorEmailAddress = emailAddress } if let emailAddress = authorEmailAddress, emailAddress.contains(" ") { byline += emailAddress // probably name plus email address } else if let name = author.name, let emailAddress = authorEmailAddress { byline += "\(name) <\(emailAddress)>" } else if let name = author.name { byline += name } else if let emailAddress = authorEmailAddress { byline += "<\(emailAddress)>" } else if let url = author.url { byline += url } } return byline } } // MARK: Path struct ArticlePathKey { static let accountID = "accountID" static let accountName = "accountName" static let feedID = "feedID" static let articleID = "articleID" } extension Article { @MainActor public var pathUserInfo: [AnyHashable : Any] { return [ ArticlePathKey.accountID: accountID, ArticlePathKey.accountName: account?.nameForDisplay ?? "", ArticlePathKey.feedID: feedID, ArticlePathKey.articleID: articleID ] } } // MARK: SortableArticle extension Article: SortableArticle { @MainActor var sortableName: String { return feed?.name ?? "" } var sortableDate: Date { return logicalDatePublished } var sortableArticleID: String { return articleID } var sortableFeedID: String { return feedID } }