This commit is contained in:
Maurice Parker
2023-12-16 09:48:30 -06:00
43 changed files with 754 additions and 431 deletions

View File

@@ -12,7 +12,7 @@ let package = Package(
targets: ["Account"]),
],
dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),

View File

@@ -428,23 +428,16 @@ public enum FetchType {
await delegate.receiveRemoteNotification(for: self, userInfo: userInfo)
}
public func refreshAll(completion: @escaping (Result<Void, Error>) -> Void) {
delegate.refreshAll(for: self, completion: completion)
public func refreshAll() async throws {
try await delegate.refreshAll(for: self)
}
public func sendArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {
delegate.sendArticleStatus(for: self) { result in
switch result {
case .success:
completion?(.success(()))
case .failure(let error):
completion?(.failure(error))
}
}
public func sendArticleStatus() async throws {
try await delegate.sendArticleStatus(for: self)
}
public func syncArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {
delegate.syncArticleStatus(for: self, completion: completion)
public func syncArticleStatus() async throws {
try await delegate.syncArticleStatus(for: self)
}
public func importOPML(_ opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
@@ -453,18 +446,23 @@ public enum FetchType {
return
}
delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in
switch result {
case .success:
guard let self = self else { return }
// Reset the last fetch date to get the article history for the added feeds.
self.metadata.lastArticleFetchStartTime = nil
self.delegate.refreshAll(for: self, completion: completion)
case .failure(let error):
completion(.failure(error))
delegate.importOPML(for: self, opmlFile: opmlFile) { result in
Task { @MainActor in
switch result {
case .success:
// Reset the last fetch date to get the article history for the added feeds.
self.metadata.lastArticleFetchStartTime = nil
do {
try await self.delegate.refreshAll(for: self)
completion(result)
} catch {
completion(.failure(error))
}
case .failure:
completion(result)
}
}
}
}
public func suspendNetwork() {

View File

@@ -25,10 +25,10 @@ import Secrets
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any]) async
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void)
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)?)
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
func refreshAll(for account: Account) async throws
func syncArticleStatus(for account: Account) async throws
func sendArticleStatus(for account: Account) async throws
func refreshArticleStatus(for account: Account) async throws
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void)

View File

@@ -25,7 +25,7 @@ public enum FeedbinAccountDelegateError: String, Error {
@MainActor final class FeedbinAccountDelegate: AccountDelegate, Logging {
private let database: SyncDatabase
private let caller: FeedbinAPICaller
let behaviors: AccountBehaviors = [.disallowFeedCopyInRootFolder]
@@ -75,58 +75,73 @@ public enum FeedbinAccountDelegateError: String, Error {
return
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
func refreshAll(for account: Account) async throws {
refreshProgress.addToNumberOfTasksAndRemaining(5)
refreshAccount(account) { result in
switch result {
case .success():
try await withCheckedThrowingContinuation { continuation in
refreshAccount(account) { result in
switch result {
case .success():
self.refreshArticlesAndStatuses(account) { result in
switch result {
case .success():
completion(.success(()))
case .failure(let error):
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
completion(.failure(wrappedError))
self.refreshArticlesAndStatuses(account) { result in
switch result {
case .success():
continuation.resume()
case .failure(let error):
Task { @MainActor in
self.refreshProgress.clear()
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
continuation.resume(throwing: wrappedError)
}
}
}
}
case .failure(let error):
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
completion(.failure(wrappedError))
}
}
}
}
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
completion?(.success(()))
case .failure(let error):
completion?(.failure(error))
case .failure(let error):
Task { @MainActor in
self.refreshProgress.clear()
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
continuation.resume(throwing: wrappedError)
}
}
case .failure(let error):
completion?(.failure(error))
}
}
}
func syncArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
func sendArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.sendArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
Task { @MainActor in
logger.debug("Sending article statuses")
@@ -188,7 +203,21 @@ public enum FeedbinAccountDelegateError: String, Error {
}
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
func refreshArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
logger.debug("Refreshing article statuses...")

View File

@@ -24,7 +24,7 @@ public enum LocalAccountDelegateError: String, Error {
final class LocalAccountDelegate: AccountDelegate, Logging {
weak var account: Account?
private lazy var refresher: LocalAccountRefresher? = {
let refresher = LocalAccountRefresher()
refresher.delegate = self
@@ -44,43 +44,46 @@ final class LocalAccountDelegate: AccountDelegate, Logging {
return
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
func refreshAll(for account: Account) async throws {
guard refreshProgress.isComplete else {
completion(.success(()))
return
}
let feeds = account.flattenedFeeds()
let feedURLs = Set(feeds.map{ $0.url })
refreshProgress.addToNumberOfTasksAndRemaining(feedURLs.count)
try await withCheckedThrowingContinuation { continuation in
Task { @MainActor in
let feeds = account.flattenedFeeds()
let feedURLs = Set(feeds.map{ $0.url })
refreshProgress.addToNumberOfTasksAndRemaining(feedURLs.count)
let group = DispatchGroup()
let group = DispatchGroup()
group.enter()
refresher?.refreshFeedURLs(feedURLs) {
group.leave()
}
group.enter()
refresher?.refreshFeedURLs(feedURLs) {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
self.refreshProgress.clear()
account.metadata.lastArticleFetchEndTime = Date()
completion(.success(()))
}
group.notify(queue: DispatchQueue.main) {
self.refreshProgress.clear()
account.metadata.lastArticleFetchEndTime = Date()
continuation.resume()
}
}
}
}
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
completion?(.success(()))
func syncArticleStatus(for account: Account) async throws {
return
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
completion(.success(()))
func sendArticleStatus(for account: Account) async throws {
return
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
completion(.success(()))
func refreshArticleStatus(for account: Account) async throws {
return
}
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
var fileData: Data?

View File

@@ -469,9 +469,9 @@ extension NewsBlurAccountDelegate {
// Download the initial articles
downloadFeed(account: account, feed: feed, page: 1) { result in
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
Task { @MainActor in
do {
try await self.refreshArticleStatus(for: account)
self.refreshMissingStories(for: account) { result in
switch result {
case .success:
@@ -485,8 +485,7 @@ extension NewsBlurAccountDelegate {
completion(.failure(error))
}
}
case .failure(let error):
} catch {
completion(.failure(error))
}
}

View File

@@ -66,7 +66,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
return
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
private func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
self.refreshProgress.addToNumberOfTasksAndRemaining(4)
refreshFeeds(for: account) { result in
@@ -118,25 +118,54 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
}
}
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
completion?(.success(()))
case .failure(let error):
completion?(.failure(error))
}
func refreshAll(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.refreshAll(for: account) { result in
switch result {
case .success():
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
func syncArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
case .failure(let error):
continuation.resume(throwing: error)
}
case .failure(let error):
completion?(.failure(error))
}
}
}
func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
func sendArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.sendArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
Task { @MainActor in
logger.debug("Sending story statuses")
@@ -208,7 +237,21 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
}
}
func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
func refreshArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
logger.debug("Refreshing story statuses...")
let group = DispatchGroup()

View File

@@ -39,7 +39,7 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
@MainActor final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
private let variant: ReaderAPIVariant
private let database: SyncDatabase
private let caller: ReaderAPICaller
@@ -166,35 +166,61 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
}
}
}
}
}
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
guard variant != .inoreader else {
completion?(.success(()))
return
}
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
completion?(.success(()))
case .failure(let error):
completion?(.failure(error))
}
func refreshAll(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.refreshAll(for: account) { result in
switch result {
case .success():
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
case .failure(let error):
completion?(.failure(error))
}
}
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
func syncArticleStatus(for account: Account) async throws {
guard variant != .inoreader else {
return
}
try await withCheckedThrowingContinuation { continuation in
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
func sendArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.sendArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
logger.debug("Sending article statuses")
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
@@ -241,7 +267,21 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
}
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
func refreshArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
logger.debug("Refreshing article statuses...")
let group = DispatchGroup()

View File

@@ -245,37 +245,34 @@ import RSDatabase
}
}
public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void, completion: (() -> Void)? = nil) {
public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void) async {
guard let reachability = try? Reachability(hostname: "apple.com"), reachability.connection != .unavailable else { return }
let group = DispatchGroup()
for account in activeAccounts {
group.enter()
account.refreshAll() { result in
group.leave()
switch result {
case .success:
break
case .failure(let error):
Task { @MainActor in
errorHandler(error)
}
await withTaskGroup(of: Void.self) { group in
for account in activeAccounts {
group.addTask {
do {
try await account.refreshAll()
} catch {
Task { @MainActor in
errorHandler(error)
}
}
}
}
}
group.notify(queue: DispatchQueue.main) {
completion?()
await group.waitForAll()
}
}
public func sendArticleStatusAll(completion: (() -> Void)? = nil) {
let group = DispatchGroup()
for account in activeAccounts {
group.enter()
account.sendArticleStatus() { _ in
Task { @MainActor in
try? await account.sendArticleStatus()
group.leave()
}
}
@@ -285,18 +282,17 @@ import RSDatabase
}
}
public func syncArticleStatusAll(completion: (() -> Void)? = nil) {
let group = DispatchGroup()
for account in activeAccounts {
group.enter()
account.syncArticleStatus() { _ in
group.leave()
}
}
group.notify(queue: DispatchQueue.global(qos: .background)) {
completion?()
public func syncArticleStatusAll() async {
await withTaskGroup(of: Void.self) { group in
for account in activeAccounts {
group.addTask {
try? await account.syncArticleStatus()
}
}
await group.waitForAll()
}
}

View File

@@ -78,56 +78,54 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging {
}
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
func refreshAll(for account: Account) async throws {
guard refreshProgress.isComplete else {
completion(.success(()))
return
}
let reachability = SCNetworkReachabilityCreateWithName(nil, "apple.com")
var flags = SCNetworkReachabilityFlags()
guard SCNetworkReachabilityGetFlags(reachability!, &flags), flags.contains(.reachable) else {
completion(.success(()))
return
}
standardRefreshAll(for: account, completion: completion)
try await withCheckedThrowingContinuation { continuation in
standardRefreshAll(for: account) { result in
switch result {
case .success():
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
completion?(.success(()))
case .failure(let error):
completion?(.failure(error))
}
func syncArticleStatus(for account: Account) async throws {
try await sendArticleStatus(for: account)
}
func sendArticleStatus(for account: Account) async throws {
try await sendArticleStatus(for: account, showProgress: false)
}
func refreshArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
let op = CloudKitReceiveStatusOperation(articlesZone: self.articlesZone)
op.completionBlock = { mainThreadOperaion in
if mainThreadOperaion.isCanceled {
continuation.resume(throwing: CloudKitAccountDelegateError.unknown)
} else {
continuation.resume()
}
case .failure(let error):
completion?(.failure(error))
}
mainThreadOperationQueue.add(op)
}
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
sendArticleStatus(for: account, showProgress: false, completion: completion)
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
let op = CloudKitReceiveStatusOperation(articlesZone: articlesZone)
op.completionBlock = { mainThreadOperaion in
if mainThreadOperaion.isCanceled {
completion(.failure(CloudKitAccountDelegateError.unknown))
} else {
completion(.success(()))
}
}
mainThreadOperationQueue.add(op)
}
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
guard refreshProgress.isComplete else {
completion(.success(()))
@@ -444,7 +442,7 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging {
try? await self.database.insertStatuses(syncStatuses)
if let count = try? await self.database.selectPendingCount(), count > 100 {
self.sendArticleStatus(for: account, showProgress: false) { _ in }
try? await self.sendArticleStatus(for: account, showProgress: false)
}
completion(.success(()))
case .failure(let error):
@@ -507,52 +505,92 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging {
}
private extension CloudKitAccountDelegate {
func initialRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
func fail(_ error: Error) {
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
}
refreshProgress.isIndeterminate = true
refreshProgress.addToNumberOfTasksAndRemaining(3)
accountZone.fetchChangesInZone() { result in
self.refreshProgress.completeTask()
let feeds = account.flattenedFeeds()
self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
func fetchChangesInZone() async throws {
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
self.refreshProgress.completeTask()
self.refreshProgress.isIndeterminate = false
switch result {
case .success:
self.combinedRefresh(account, feeds) { result in
self.refreshProgress.clear()
switch result {
case .success:
account.metadata.lastArticleFetchEndTime = Date()
case .failure(let error):
fail(error)
}
}
case .failure(let error):
fail(error)
}
try await withCheckedThrowingContinuation { continuation in
self.accountZone.fetchChangesInZone { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
case .failure(let error):
fail(error)
}
}
}
@MainActor func initialRefreshAll(for account : Account) async throws {
refreshProgress.isIndeterminate = true
refreshProgress.addToNumberOfTasksAndRemaining(3)
do {
try await fetchChangesInZone()
refreshProgress.completeTask()
let feeds = account.flattenedFeeds()
refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
try await refreshArticleStatus(for: account)
refreshProgress.completeTask()
refreshProgress.isIndeterminate = false
await combinedRefresh(account, feeds)
refreshProgress.clear()
account.metadata.lastArticleFetchEndTime = Date()
} catch {
processAccountError(account, error)
refreshProgress.clear()
throw error
}
}
// func initialRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
//
// func fail(_ error: Error) {
// self.processAccountError(account, error)
// self.refreshProgress.clear()
// completion(.failure(error))
// }
//
// refreshProgress.isIndeterminate = true
// refreshProgress.addToNumberOfTasksAndRemaining(3)
// accountZone.fetchChangesInZone() { result in
// self.refreshProgress.completeTask()
//
// let feeds = account.flattenedFeeds()
// self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
//
// switch result {
// case .success:
// self.refreshArticleStatus(for: account) { result in
// self.refreshProgress.completeTask()
// self.refreshProgress.isIndeterminate = false
// switch result {
// case .success:
//
// self.combinedRefresh(account, feeds) { result in
// self.refreshProgress.clear()
// switch result {
// case .success:
// account.metadata.lastArticleFetchEndTime = Date()
// case .failure(let error):
// fail(error)
// }
// }
//
// case .failure(let error):
// fail(error)
// }
// }
// case .failure(let error):
// fail(error)
// }
// }
//
// }
func standardRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
let intialFeedsCount = account.flattenedFeeds().count
@@ -573,6 +611,36 @@ private extension CloudKitAccountDelegate {
let feeds = account.flattenedFeeds()
self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count - intialFeedsCount)
Task { @MainActor in
do {
try await self.refreshArticleStatus(for: account)
self.refreshProgress.completeTask()
self.refreshProgress.isIndeterminate = false
self.combinedRefresh(account, feeds) { result in
do {
try await self.sendArticleStatus(for: account, showProgress: true)
if case .failure(let error) = result {
fail(error)
} else {
account.metadata.lastArticleFetchEndTime = Date()
completion(.success(()))
}
} catch {
fail(error)
}
}
} catch {
self.refreshProgress.completeTask()
self.refreshProgress.isIndeterminate = false
}
}
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
@@ -601,20 +669,31 @@ private extension CloudKitAccountDelegate {
}
func combinedRefresh(_ account: Account, _ feeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
func combinedRefresh(_ account: Account, _ feeds: Set<Feed>) async {
let feedURLs = Set(feeds.map{ $0.url })
let group = DispatchGroup()
try await withCheckedContinuation { continuation in
refresher.refreshFeedURLs(feedURLs) {
continuation.resume()
}
}
}
group.enter()
refresher.refreshFeedURLs(feedURLs) {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion(.success(()))
}
}
// func combinedRefresh(_ account: Account, _ feeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
//
// let feedURLs = Set(feeds.map{ $0.url })
// let group = DispatchGroup()
//
// group.enter()
// refresher.refreshFeedURLs(feedURLs) {
// group.leave()
// }
//
// group.notify(queue: DispatchQueue.main) {
// completion(.success(()))
// }
// }
func createRSSFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
@@ -729,12 +808,13 @@ private extension CloudKitAccountDelegate {
case .success(let articles):
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
self.refreshProgress.completeTask()
self.sendArticleStatus(for: account, showProgress: true) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { _ in }
case .failure(let error):
self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)")
Task { @MainActor in
do {
try await self.sendArticleStatus(for: account, showProgress: true)
try await self.refreshArticleStatus(for: account)
} catch {
self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)")
}
}
}
@@ -795,20 +875,24 @@ private extension CloudKitAccountDelegate {
}
}
func sendArticleStatus(for account: Account, showProgress: Bool, completion: @escaping ((Result<Void, Error>) -> Void)) {
let op = CloudKitSendStatusOperation(account: account,
articlesZone: articlesZone,
refreshProgress: refreshProgress,
showProgress: showProgress,
database: database)
op.completionBlock = { mainThreadOperaion in
if mainThreadOperaion.isCanceled {
completion(.failure(CloudKitAccountDelegateError.unknown))
} else {
completion(.success(()))
func sendArticleStatus(for account: Account, showProgress: Bool) async throws {
try await withCheckedThrowingContinuation { continuation in
let op = CloudKitSendStatusOperation(account: account,
articlesZone: self.articlesZone,
refreshProgress: self.refreshProgress,
showProgress: showProgress,
database: self.database)
op.completionBlock = { mainThreadOperation in
if mainThreadOperation.isCanceled {
continuation.resume(throwing: CloudKitAccountDelegateError.unknown)
} else {
continuation.resume()
}
}
mainThreadOperationQueue.add(op)
}
mainThreadOperationQueue.add(op)
}

View File

@@ -253,6 +253,29 @@ final class CloudKitAccountZone: CloudKitZone {
}
}
func findOrCreateAccount() async throws -> String {
guard let database else {
return
}
let predicate = NSPredicate(format: "isAccount = \"1\"")
let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate)
do {
let records = try await database.perform(ckQuery, inZoneWith: zoneID)
if records.count > 0 {
return records[0].externalID
} else {
createContainer(name: "Account", isAccount: true, completion: completion)
}
} catch {
switch CloudKitZoneResult.resolve(error) {
}
}
}
func findOrCreateAccount(completion: @escaping (Result<String, Error>) -> Void) {
let predicate = NSPredicate(format: "isAccount = \"1\"")
let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate)
@@ -343,6 +366,21 @@ private extension CloudKitAccountZone {
return record
}
func createContainer(name: String, isAccount: Bool) async throws -> String {
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
record[CloudKitContainer.Fields.name] = name
record[CloudKitContainer.Fields.isAccount] = isAccount ? "1" : "0"
save(record) { result in
switch result {
case .success:
completion(.success(record.externalID))
case .failure(let error):
completion(.failure(error))
}
}
}
func createContainer(name: String, isAccount: Bool, completion: @escaping (Result<String, Error>) -> Void) {
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
record[CloudKitContainer.Fields.name] = name

View File

@@ -31,32 +31,19 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
self.articlesZone = articlesZone
}
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
for deletedRecordKey in deleted {
switch deletedRecordKey.recordType {
case CloudKitAccountZone.CloudKitFeed.recordType:
removeFeed(deletedRecordKey.recordID.externalID)
case CloudKitAccountZone.CloudKitContainer.recordType:
removeContainer(deletedRecordKey.recordID.externalID)
default:
assertionFailure("Unknown record type: \(deletedRecordKey.recordType)")
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws {
try await withCheckedThrowingContinuation { continuation in
self.cloudKitWasChanged(updated: updated, deleted: deleted) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
for changedRecord in updated {
switch changedRecord.recordType {
case CloudKitAccountZone.CloudKitFeed.recordType:
addOrUpdateFeed(changedRecord)
case CloudKitAccountZone.CloudKitContainer.recordType:
addOrUpdateContainer(changedRecord)
default:
assertionFailure("Unknown record type: \(changedRecord.recordType)")
}
}
completion(.success(()))
}
@MainActor func addOrUpdateFeed(_ record: CKRecord) {
guard let account = account,
let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String,
@@ -140,7 +127,33 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
private extension CloudKitAcountZoneDelegate {
@MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
for deletedRecordKey in deleted {
switch deletedRecordKey.recordType {
case CloudKitAccountZone.CloudKitFeed.recordType:
removeFeed(deletedRecordKey.recordID.externalID)
case CloudKitAccountZone.CloudKitContainer.recordType:
removeContainer(deletedRecordKey.recordID.externalID)
default:
assertionFailure("Unknown record type: \(deletedRecordKey.recordType)")
}
}
for changedRecord in updated {
switch changedRecord.recordType {
case CloudKitAccountZone.CloudKitFeed.recordType:
addOrUpdateFeed(changedRecord)
case CloudKitAccountZone.CloudKitContainer.recordType:
addOrUpdateContainer(changedRecord)
default:
assertionFailure("Unknown record type: \(changedRecord.recordType)")
}
}
completion(.success(()))
}
@MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
guard let account = account else { return }
feed.name = name

View File

@@ -84,7 +84,30 @@ final class CloudKitArticlesZone: CloudKitZone {
self.save(compressedRecords, completion: completion)
}
}
@MainActor func saveNewArticles(_ articles: Set<Article>) async throws {
guard !articles.isEmpty else {
return
}
let records: [CKRecord] = {
var recordsAccumulator = [CKRecord]()
let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true }
for saveArticle in saveArticles {
recordsAccumulator.append(makeStatusRecord(saveArticle))
recordsAccumulator.append(makeArticleRecord(saveArticle))
}
return recordsAccumulator
}()
compressionQueue.async {
let compressedRecords = self.compressArticleRecords(records)
self.save(compressedRecords, completion: completion)
}
}
func deleteArticles(_ feedExternalID: String, completion: @escaping ((Result<Void, Error>) -> Void)) {
let predicate = NSPredicate(format: "webFeedExternalID = %@", feedExternalID)
let ckQuery = CKQuery(recordType: CloudKitArticleStatus.recordType, predicate: predicate)

View File

@@ -28,6 +28,22 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
self.articlesZone = articlesZone
}
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws {
try await withCheckedThrowingContinuation { continuation in
cloudKitWasChanged(updated: updated, deleted: deleted) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
private extension CloudKitArticlesZoneDelegate {
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
Task { @MainActor in
@@ -54,9 +70,6 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
}
}
}
}
private extension CloudKitArticlesZoneDelegate {
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Error?) -> Void) {
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })

View File

@@ -32,16 +32,15 @@ class CloudKitReceiveStatusOperation: MainThreadOperation, Logging {
logger.debug("Refreshing article statuses...")
articlesZone.fetchChangesInZone() { result in
self.logger.debug("Done refreshing article statuses.")
switch result {
case .success:
Task { @MainActor in
do {
try await articlesZone.fetchChangesInZone()
self.logger.debug("Done refreshing article statuses.")
self.operationDelegate?.operationDidComplete(self)
case .failure(let error):
self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)")
} catch {
self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)")
self.operationDelegate?.cancelOperation(self)
}
}
}
}

View File

@@ -36,13 +36,11 @@ class CloudKitRemoteNotificationOperation: MainThreadOperation, Logging {
logger.debug("Processing remote notification...")
accountZone.receiveRemoteNotification(userInfo: userInfo) {
articlesZone.receiveRemoteNotification(userInfo: self.userInfo) {
self.logger.debug("Done processing remote notification.")
self.operationDelegate?.operationDidComplete(self)
}
Task { @MainActor in
await accountZone.receiveRemoteNotification(userInfo: self.userInfo)
await articlesZone.receiveRemoteNotification(userInfo: self.userInfo)
self.logger.debug("Done processing remote notification.")
self.operationDelegate?.operationDidComplete(self)
}
}
}

View File

@@ -107,19 +107,17 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
return
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
func refreshAll(for account: Account) async throws {
assert(Thread.isMainThread)
guard currentSyncAllOperation == nil else {
self.logger.debug("Ignoring refreshAll: Feedly sync already in progress.")
completion(.success(()))
return
}
guard let credentials = credentials else {
self.logger.debug("Ignoring refreshAll: Feedly account has no credentials.")
completion(.failure(FeedlyAccountDelegateError.notLoggedIn))
return
throw FeedlyAccountDelegateError.notLoggedIn
}
let syncAllOperation = FeedlySyncAllOperation(account: account, feedlyUserID: credentials.username, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress)
@@ -127,51 +125,62 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
syncAllOperation.downloadProgress = refreshProgress
let date = Date()
syncAllOperation.syncCompletionHandler = { [weak self] result in
if case .success = result {
self?.accountMetadata?.lastArticleFetchStartTime = date
self?.accountMetadata?.lastArticleFetchEndTime = Date()
}
self?.logger.debug("Sync took \(-date.timeIntervalSinceNow, privacy: .public) seconds.")
completion(result)
}
currentSyncAllOperation = syncAllOperation
operationQueue.add(syncAllOperation)
}
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
completion?(.success(()))
case .failure(let error):
self.logger.error("Failed to refresh article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)")
completion?(.failure(error))
}
try await withCheckedThrowingContinuation { continuation in
syncAllOperation.syncCompletionHandler = { result in
self.logger.debug("Sync took \(-date.timeIntervalSinceNow, privacy: .public) seconds.")
switch result {
case .success():
self.accountMetadata?.lastArticleFetchStartTime = date
self.accountMetadata?.lastArticleFetchEndTime = Date()
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
case .failure(let error):
self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)")
completion?(.failure(error))
}
currentSyncAllOperation = syncAllOperation
operationQueue.add(syncAllOperation)
}
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
func syncArticleStatus(for account: Account) async throws {
do {
try await sendArticleStatus(for: account)
try await refreshArticleStatus(for: account)
} catch {
self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)")
throw error
}
}
func sendArticleStatus(for account: Account) async throws {
// Ensure remote articles have the same status as they do locally.
let send = FeedlySendArticleStatusesOperation(database: database, service: caller)
send.completionBlock = { operation in
// TODO: not call with success if operation was canceled? Not sure.
DispatchQueue.main.async {
completion(.success(()))
try await withCheckedThrowingContinuation { continuation in
let send = FeedlySendArticleStatusesOperation(database: database, service: caller)
send.completionBlock = { operation in
continuation.resume()
}
operationQueue.add(send)
}
}
func refreshArticleStatus(for account: Account) async throws {
try await withCheckedThrowingContinuation { continuation in
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
operationQueue.add(send)
}
/// Attempts to ensure local articles have the same status as they do remotely.
@@ -180,7 +189,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
///
/// - Parameter account: The account whose articles have a remote status.
/// - Parameter completion: Call on the main queue.
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
private func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard let credentials = credentials else {
return completion(.success(()))
}
@@ -550,7 +559,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
try await self.database.insertStatuses(syncStatuses)
let count = try await self.database.selectPendingCount()
if count > 100 {
self.sendArticleStatus(for: account) { _ in }
try? await self.sendArticleStatus(for: account)
}
completion(.success(()))
} catch {

View File

@@ -6,7 +6,21 @@
<description>Most recent NetNewsWire changes with links to updates.</description>
<language>en</language>
<item>
<item>
<title>NetNewsWire 6.1.5b1</title>
<description><![CDATA[
<p>This builds adds a new setting — you can turn on/off JavaScript for the article pane. Its on by default, which matches previous behavior.</p>
<p>Note that some content — videos and embedded social media posts, for instance — will often require JavaScript to be <i>on</i> in order to work properly.</p>
<p>However, for those who want or need greater security and privacy, weve made this setting available.</p>
<p>This build also fixes a case where images might not load in the article pane.</p>
]]></description>
<pubDate>Fri, 01 Dec 2023 17:30:00 -0700</pubDate>
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1.5b1/NetNewsWire6.1.5b1.zip" sparkle:version="6121" sparkle:shortVersionString="6.1.5b1" length="10098688" type="application/zip" />
<sparkle:minimumSystemVersion>11.0.0</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 6.1.4b1</title>
<description><![CDATA[
<p><b>This build removes Reddit API integration! Dont install it if youre not ready for that to happen!</b></p>

View File

@@ -11,7 +11,7 @@ let package = Package(
targets: ["Articles"]),
],
dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
],
targets: [
.target(

View File

@@ -4,7 +4,7 @@
import PackageDescription
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
]

View File

@@ -13,7 +13,7 @@ let package = Package(
targets: ["FeedFinder"]),
],
dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(path: "../AccountError"),

View File

@@ -581,7 +581,9 @@ var appDelegate: AppDelegate!
}
@IBAction func refreshAll(_ sender: Any?) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
Task {
await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
}
}
@IBAction func showAddFeedWindow(_ sender: Any?) {

View File

@@ -100,6 +100,18 @@ protocol DetailWebViewControllerDelegate: AnyObject {
userContentController.add(self, name: MessageName.windowDidScroll)
userContentController.add(self, name: MessageName.mouseDidEnter)
userContentController.add(self, name: MessageName.mouseDidExit)
let baseURL = ArticleRenderer.page.baseURL
let appScriptsWorld = WKContentWorld.world(name: "NetNewsWire")
for fileName in ["main.js", "main_mac.js", "newsfoot.js"] {
userContentController.addUserScript(
.init(source: try! String(contentsOf: baseURL.appending(path: fileName,
directoryHint: .notDirectory)),
injectionTime: .atDocumentStart,
forMainFrameOnly: true,
in: appScriptsWorld))
}
configuration.userContentController = userContentController
webView = DetailWebView(frame: NSRect.zero, configuration: configuration)
@@ -323,7 +335,7 @@ private extension DetailWebViewController {
]
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL)
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
}
func scrollInfo() async -> ScrollInfo? {

View File

@@ -4,14 +4,6 @@
<style>
[[style]]
</style>
<script src="main.js"></script>
<script src="main_mac.js"></script>
<script src="newsfoot.js" async="async"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
processPage();
})
</script>
<base href="[[baseURL]]">
</head>
<body>

View File

@@ -14,19 +14,31 @@ import Foundation
// Since the Toolbar intercepts right clicks we need to stop it from doing that here
// so that the ArticleExtractorButton can receive right click events.
if event.isRightClick,
let frameView = contentView?.superview,
let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)),
type(of: view).description() == "NSToolbarView" {
if #available(macOS 14.0, *) {
if event.isRightClick,
let frameView = contentView?.superview,
let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)),
let articleExtractorButton = view as? ArticleExtractorButton {
for subview in view.subviews {
for subsubview in subview.subviews {
let candidateView = subsubview.hitTest(subsubview.convert(event.locationInWindow, from: nil))
if candidateView is ArticleExtractorButton {
candidateView?.rightMouseDown(with: event)
return
articleExtractorButton.rightMouseDown(with: event)
return
}
} else {
if event.isRightClick,
let frameView = contentView?.superview,
let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)),
type(of: view).description() == "NSToolbarView" {
for subview in view.subviews {
for subsubview in subview.subviews {
let candidateView = subsubview.hitTest(subsubview.convert(event.locationInWindow, from: nil))
if candidateView is ArticleExtractorButton {
candidateView?.rightMouseDown(with: event)
return
}
}
}
}
}

View File

@@ -94,13 +94,10 @@ import Secrets
try self.account?.removeCredentials(type: .basic)
try self.account?.storeCredentials(validatedCredentials)
self.account?.refreshAll() { result in
switch result {
case .success:
break
case .failure(let error):
NSApplication.shared.presentError(error)
}
do {
try await self.account?.refreshAll()
} catch {
NSApplication.shared.presentError(error)
}
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)

View File

@@ -92,13 +92,10 @@ import Secrets
try self.account?.storeCredentials(credentials)
try self.account?.storeCredentials(validatedCredentials)
self.account?.refreshAll() { result in
switch result {
case .success:
break
case .failure(let error):
NSApplication.shared.presentError(error)
}
do {
try await self.account?.refreshAll()
} catch {
NSApplication.shared.presentError(error)
}
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)

View File

@@ -272,12 +272,11 @@ extension AccountsPreferencesViewController: OAuthAccountAuthorizationOperationD
// because the user probably wants to see the result of authorizing NetNewsWire to act on their behalf.
NSApp.activate(ignoringOtherApps: true)
account.refreshAll { [weak self] result in
switch result {
case .success:
break
case .failure(let error):
self?.presentError(error)
Task {
do {
try await account.refreshAll()
} catch {
self.presentError(error)
}
}
}

View File

@@ -151,13 +151,10 @@ import ReaderAPI
try self.account?.storeCredentials(credentials)
try self.account?.storeCredentials(validatedCredentials)
self.account?.refreshAll() { result in
switch result {
case .success:
break
case .failure(let error):
NSApplication.shared.presentError(error)
}
do {
try await self.account?.refreshAll()
} catch {
NSApplication.shared.presentError(error)
}
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)

View File

@@ -5296,7 +5296,7 @@
repositoryURL = "https://github.com/Ranchero-Software/RSCore.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.0.1;
minimumVersion = 3.0.0;
};
};
510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = {

View File

@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Ranchero-Software/RSCore.git",
"state" : {
"revision" : "dcaa40ceb2c8acd182fbcd69d1f8e56df97e38b1",
"version" : "2.0.1"
"revision" : "cee6d96e036cc4ad08ad5f79364f87f9c291c43c",
"version" : "3.0.0"
}
},
{

View File

@@ -6,7 +6,7 @@ It supports [RSS](https://cyber.harvard.edu/rss/rss.html), [Atom](https://datatr
More info: [https://netnewswire.com/](https://netnewswire.com/)
Also see the [Technotes](Technotes/) and the [Roadmap](Technotes/Roadmap.md).
Also see the [Technotes](Technotes/) and the [Roadmap/Milestones](https://github.com/Ranchero-Software/NetNewsWire/milestones).
Note: NetNewsWires Help menu has a bunch of these links, so you dont have to remember to come back to this page.

View File

@@ -168,3 +168,8 @@ function processPage() {
removeWpSmiley()
postRenderProcessing();
}
document.addEventListener("DOMContentLoaded", function(event) {
window.scrollTo(0, [[windowScrollY]]);
processPage();
})

View File

@@ -263,6 +263,10 @@ figure {
margin-top: 1em;
}
figure > * + * {
margin-top: 0.5em;
}
figcaption {
font-size: 14px;
line-height: 1.3em;

View File

@@ -73,6 +73,8 @@ import Account
lastTimedRefresh = Date()
update()
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil)
Task {
await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
}
}
}

View File

@@ -68,8 +68,8 @@ class ArticleStatusSyncTimer {
lastTimedRefresh = Date()
update()
AccountManager.shared.syncArticleStatusAll()
Task {
await AccountManager.shared.syncArticleStatusAll()
}
}
}

View File

@@ -15,7 +15,7 @@ let package = Package(
dependencies: [
.package(path: "../../Secrets"),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2"))
],
targets: [

View File

@@ -13,7 +13,7 @@ let package = Package(
targets: ["LocalAccount"]),
],
dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
],

View File

@@ -15,7 +15,7 @@ let package = Package(
dependencies: [
.package(path: "../../Secrets"),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2"))
],
targets: [

View File

@@ -16,7 +16,7 @@ let package = Package(
.package(path: "../../Secrets"),
.package(path: "../../AccountError"),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2"))
],
targets: [

View File

@@ -2,7 +2,7 @@
import PackageDescription
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")),
]

View File

@@ -529,6 +529,20 @@ private extension WebViewController {
}
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
let userContentController = WKUserContentController()
let baseURL = ArticleRenderer.page.baseURL
let appScriptsWorld = WKContentWorld.world(name: "NetNewsWire")
for fileName in ["main.js", "main_ios.js", "newsfoot.js"] {
userContentController.addUserScript(
.init(source: try! String(contentsOf: baseURL.appending(path: fileName,
directoryHint: .notDirectory)),
injectionTime: .atDocumentStart,
forMainFrameOnly: true,
in: appScriptsWorld))
}
configuration.userContentController = userContentController
let webView = WKWebView(frame: self.view.bounds, configuration: configuration)
webView.isOpaque = false;
webView.backgroundColor = .clear;
@@ -591,8 +605,8 @@ private extension WebViewController {
]
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL)
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
}
func finalScrollPosition(scrollingUp: Bool) -> CGFloat {

View File

@@ -5,15 +5,6 @@
<style>
[[style]]
</style>
<script src="main.js"></script>
<script src="main_ios.js"></script>
<script src="newsfoot.js" async="async"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
window.scrollTo(0, [[windowScrollY]]);
processPage();
})
</script>
<base href="[[baseURL]]">
</head>
<body>