2017-05-27 10:43:27 -07:00
|
|
|
//
|
|
|
|
// ArticleUtilities.swift
|
2018-08-28 22:18:24 -07:00
|
|
|
// NetNewsWire
|
2017-05-27 10:43:27 -07:00
|
|
|
//
|
|
|
|
// Created by Brent Simmons on 7/25/15.
|
|
|
|
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
2018-07-23 18:29:08 -07:00
|
|
|
import Articles
|
2017-09-17 12:34:10 -07:00
|
|
|
import Account
|
2024-04-15 22:21:17 -07:00
|
|
|
import Images
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
|
|
// These handle multiple accounts.
|
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
2017-09-17 12:54:08 -07:00
|
|
|
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
|
2017-10-29 11:14:10 -07:00
|
|
|
|
2021-04-12 19:41:01 -05:00
|
|
|
let group = DispatchGroup()
|
|
|
|
|
2017-10-08 21:06:25 -07:00
|
|
|
for (accountID, accountArticles) in d {
|
|
|
|
guard let account = AccountManager.shared.existingAccount(with: accountID) else {
|
2017-10-29 11:14:10 -07:00
|
|
|
continue
|
2017-05-27 10:43:27 -07:00
|
|
|
}
|
2021-04-12 19:41:01 -05:00
|
|
|
group.enter()
|
2024-03-26 18:48:44 -07:00
|
|
|
Task { @MainActor in
|
|
|
|
try? await account.markArticles(accountArticles, statusKey: statusKey, flag: flag)
|
2021-04-12 19:41:01 -05:00
|
|
|
group.leave()
|
|
|
|
}
|
|
|
|
}
|
2024-03-26 18:48:44 -07:00
|
|
|
|
2021-04-12 19:41:01 -05:00
|
|
|
group.notify(queue: .main) {
|
|
|
|
completion?()
|
2017-05-27 10:43:27 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-17 12:54:08 -07:00
|
|
|
private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String: Set<Article>] {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
2017-10-08 21:06:25 -07:00
|
|
|
let d = Dictionary(grouping: articles, by: { $0.accountID })
|
|
|
|
return d.mapValues{ Set($0) }
|
2017-05-27 10:43:27 -07:00
|
|
|
}
|
|
|
|
|
2017-09-17 12:54:08 -07:00
|
|
|
extension Article {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor var feed: Feed? {
|
2024-02-25 23:12:21 -08:00
|
|
|
return account?.existingFeed(withFeedID: feedID)
|
2017-09-17 17:03:58 -07:00
|
|
|
}
|
|
|
|
|
2021-09-30 16:33:16 +13:00
|
|
|
var url: URL? {
|
2024-04-15 22:26:58 -07:00
|
|
|
return URL.repairingIfRequired(rawLink)
|
2021-09-30 16:33:16 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
var externalURL: URL? {
|
2024-04-15 22:26:58 -07:00
|
|
|
return URL.repairingIfRequired(rawExternalLink)
|
2021-09-30 16:33:16 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
var imageURL: URL? {
|
2024-04-15 22:26:58 -07:00
|
|
|
return URL.repairingIfRequired(rawImageLink)
|
2021-09-30 16:33:16 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-09-17 16:30:45 -07:00
|
|
|
var preferredLink: String? {
|
2021-09-30 16:33:16 +13:00
|
|
|
if let link = link, !link.isEmpty {
|
|
|
|
return link
|
2019-09-07 18:27:48 -07:00
|
|
|
}
|
2021-09-30 16:33:16 +13:00
|
|
|
if let externalLink = externalLink, !externalLink.isEmpty {
|
|
|
|
return externalLink
|
2019-09-07 18:27:48 -07:00
|
|
|
}
|
|
|
|
return nil
|
2017-09-17 16:30:45 -07:00
|
|
|
}
|
|
|
|
|
2021-04-26 09:28:19 +12:00
|
|
|
var preferredURL: URL? {
|
2021-09-30 16:33:16 +13:00
|
|
|
return url ?? externalURL
|
2021-04-26 09:28:19 +12:00
|
|
|
}
|
|
|
|
|
2017-09-17 16:30:45 -07:00
|
|
|
var body: String? {
|
2018-02-14 13:14:25 -08:00
|
|
|
return contentHTML ?? contentText ?? summary
|
2017-05-27 10:43:27 -07:00
|
|
|
}
|
2017-09-17 17:03:58 -07:00
|
|
|
|
|
|
|
var logicalDatePublished: Date {
|
2018-02-14 13:14:25 -08:00
|
|
|
return datePublished ?? dateModified ?? status.dateArrived
|
2017-09-17 17:03:58 -07:00
|
|
|
}
|
2020-02-18 13:49:29 -08:00
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor var isAvailableToMarkUnread: Bool {
|
2020-02-18 13:49:29 -08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2019-08-24 14:57:51 -05:00
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor func iconImage() -> IconImage? {
|
2021-05-08 12:42:44 -07:00
|
|
|
return IconImageCache.shared.imageForArticle(self)
|
2019-08-24 14:57:51 -05:00
|
|
|
}
|
2019-11-25 19:43:43 -06:00
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor func iconImageUrl(feed: Feed) -> URL? {
|
2020-12-24 07:31:44 +08:00
|
|
|
if let image = iconImage() {
|
|
|
|
let fm = FileManager.default
|
|
|
|
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
2024-02-25 23:12:21 -08:00
|
|
|
let feedID = feed.feedID.replacingOccurrences(of: "/", with: "_")
|
2020-12-24 07:31:44 +08:00
|
|
|
#if os(macOS)
|
2021-02-04 21:50:03 -06:00
|
|
|
path.appendPathComponent(feedID + "_smallIcon.tiff")
|
2020-12-24 07:31:44 +08:00
|
|
|
#else
|
2021-02-04 21:50:03 -06:00
|
|
|
path.appendPathComponent(feedID + "_smallIcon.png")
|
2020-12-24 07:31:44 +08:00
|
|
|
#endif
|
|
|
|
fm.createFile(atPath: path.path, contents: image.image.dataRepresentation()!, attributes: nil)
|
|
|
|
return path
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor func byline() -> String {
|
2024-02-25 23:12:21 -08:00
|
|
|
guard let authors = authors ?? feed?.authors, !authors.isEmpty else {
|
2019-11-25 19:43:43 -06:00
|
|
|
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 {
|
2024-02-25 23:12:21 -08:00
|
|
|
if author.name == feed?.nameForDisplay {
|
2019-11-25 19:43:43 -06:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var byline = ""
|
|
|
|
var isFirstAuthor = true
|
|
|
|
|
|
|
|
for author in authors {
|
|
|
|
if !isFirstAuthor {
|
|
|
|
byline += ", "
|
|
|
|
}
|
|
|
|
isFirstAuthor = false
|
2020-08-18 17:44:28 -05:00
|
|
|
|
|
|
|
var authorEmailAddress: String? = nil
|
|
|
|
if let emailAddress = author.emailAddress, !(emailAddress.contains("noreply@") || emailAddress.contains("no-reply@")) {
|
|
|
|
authorEmailAddress = emailAddress
|
|
|
|
}
|
2019-11-25 19:43:43 -06:00
|
|
|
|
2020-08-18 17:44:28 -05:00
|
|
|
if let emailAddress = authorEmailAddress, emailAddress.contains(" ") {
|
2019-11-25 19:43:43 -06:00
|
|
|
byline += emailAddress // probably name plus email address
|
|
|
|
}
|
2020-08-18 17:44:28 -05:00
|
|
|
else if let name = author.name, let emailAddress = authorEmailAddress {
|
2019-11-25 19:43:43 -06:00
|
|
|
byline += "\(name) <\(emailAddress)>"
|
|
|
|
}
|
|
|
|
else if let name = author.name {
|
|
|
|
byline += name
|
|
|
|
}
|
2020-08-18 17:44:28 -05:00
|
|
|
else if let emailAddress = authorEmailAddress {
|
2019-11-25 19:43:43 -06:00
|
|
|
byline += "<\(emailAddress)>"
|
|
|
|
}
|
|
|
|
else if let url = author.url {
|
|
|
|
byline += url
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return byline
|
|
|
|
}
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
}
|
2019-09-08 16:48:50 -05:00
|
|
|
|
2019-11-14 15:06:32 -06:00
|
|
|
// MARK: Path
|
2019-10-03 09:53:21 -05:00
|
|
|
|
2019-11-14 15:06:32 -06:00
|
|
|
struct ArticlePathKey {
|
|
|
|
static let accountID = "accountID"
|
|
|
|
static let accountName = "accountName"
|
2024-02-25 23:12:21 -08:00
|
|
|
static let feedID = "feedID"
|
2019-11-14 15:06:32 -06:00
|
|
|
static let articleID = "articleID"
|
|
|
|
}
|
|
|
|
|
|
|
|
extension Article {
|
2019-10-03 09:53:21 -05:00
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor public var pathUserInfo: [AnyHashable : Any] {
|
2019-10-03 09:53:21 -05:00
|
|
|
return [
|
2019-11-14 15:06:32 -06:00
|
|
|
ArticlePathKey.accountID: accountID,
|
|
|
|
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
|
2024-02-25 23:12:21 -08:00
|
|
|
ArticlePathKey.feedID: feedID,
|
2019-11-14 15:06:32 -06:00
|
|
|
ArticlePathKey.articleID: articleID
|
2019-10-03 09:53:21 -05:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-09-08 16:48:50 -05:00
|
|
|
// MARK: SortableArticle
|
|
|
|
|
|
|
|
extension Article: SortableArticle {
|
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
@MainActor var sortableName: String {
|
2024-02-25 23:12:21 -08:00
|
|
|
return feed?.name ?? ""
|
2019-09-08 16:48:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
var sortableDate: Date {
|
|
|
|
return logicalDatePublished
|
|
|
|
}
|
|
|
|
|
2019-09-13 07:43:28 -05:00
|
|
|
var sortableArticleID: String {
|
2019-09-08 16:48:50 -05:00
|
|
|
return articleID
|
|
|
|
}
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
var sortableFeedID: String {
|
|
|
|
return feedID
|
2019-09-13 07:43:28 -05:00
|
|
|
}
|
|
|
|
|
2019-09-08 16:48:50 -05:00
|
|
|
}
|