mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Continue fixing concurrency warnings.
This commit is contained in:
@@ -22,7 +22,7 @@ import RSParser
|
||||
// Else,
|
||||
// display error sheet.
|
||||
|
||||
class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
@MainActor final class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
|
||||
private let hostWindow: NSWindow
|
||||
private var addFeedWindowController: AddFeedWindowController?
|
||||
|
||||
@@ -11,7 +11,7 @@ import RSCore
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
class FolderTreeMenu {
|
||||
@MainActor final class FolderTreeMenu {
|
||||
|
||||
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {
|
||||
|
||||
|
||||
@@ -16,31 +16,32 @@ class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
|
||||
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||
|
||||
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
|
||||
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
|
||||
iconView.iconImage = iconImage
|
||||
let renderedImage = iconView.asImage()
|
||||
|
||||
guard let data = renderedImage.dataRepresentation() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
|
||||
let headerFields = ["Cache-Control": "no-cache"]
|
||||
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
|
||||
urlSchemeTask.didReceive(response)
|
||||
urlSchemeTask.didReceive(data)
|
||||
urlSchemeTask.didFinish()
|
||||
}
|
||||
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
|
||||
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
|
||||
iconView.iconImage = iconImage
|
||||
let renderedImage = iconView.asImage()
|
||||
|
||||
guard let data = renderedImage.dataRepresentation() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
|
||||
let headerFields = ["Cache-Control": "no-cache"]
|
||||
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
|
||||
urlSchemeTask.didReceive(response)
|
||||
urlSchemeTask.didReceive(data)
|
||||
urlSchemeTask.didFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
||||
urlSchemeTask.didFailWithError(URLError(.unknown))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -216,13 +216,26 @@ final class DetailWebViewController: NSViewController {
|
||||
|
||||
extension DetailWebViewController: WKScriptMessageHandler {
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
nonisolated func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
|
||||
if message.name == MessageName.windowDidScroll {
|
||||
windowScrollY = message.body as? CGFloat
|
||||
|
||||
let updatedWindowScrollY = message.body as? CGFloat
|
||||
Task { @MainActor in
|
||||
windowScrollY = updatedWindowScrollY
|
||||
}
|
||||
|
||||
} else if message.name == MessageName.mouseDidEnter, let link = message.body as? String {
|
||||
delegate?.mouseDidEnter(self, link: link)
|
||||
|
||||
Task { @MainActor in
|
||||
delegate?.mouseDidEnter(self, link: link)
|
||||
}
|
||||
|
||||
} else if message.name == MessageName.mouseDidExit {
|
||||
delegate?.mouseDidExit(self)
|
||||
|
||||
Task { @MainActor in
|
||||
delegate?.mouseDidExit(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,10 +252,13 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
||||
|
||||
// WKNavigationDelegate
|
||||
|
||||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
nonisolated public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
if navigationAction.navigationType == .linkActivated {
|
||||
if let url = navigationAction.request.url {
|
||||
self.openInBrowser(url, flags: navigationAction.modifierFlags)
|
||||
let flags = navigationAction.modifierFlags
|
||||
Task { @MainActor in
|
||||
self.openInBrowser(url, flags: flags)
|
||||
}
|
||||
}
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
@@ -251,35 +267,42 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
// See note in viewDidLoad()
|
||||
if waitingForFirstReload {
|
||||
assert(webView.isHidden)
|
||||
waitingForFirstReload = false
|
||||
reloadHTML()
|
||||
nonisolated public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
|
||||
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
|
||||
// A hard coded value is awful, but 5/100th of a second seems to be enough.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
webView.isHidden = false
|
||||
}
|
||||
} else {
|
||||
if let windowScrollY = windowScrollY {
|
||||
webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
|
||||
self.windowScrollY = nil
|
||||
Task { @MainActor in
|
||||
// See note in viewDidLoad()
|
||||
if waitingForFirstReload {
|
||||
assert(webView.isHidden)
|
||||
waitingForFirstReload = false
|
||||
reloadHTML()
|
||||
|
||||
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
|
||||
// A hard coded value is awful, but 5/100th of a second seems to be enough.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
webView.isHidden = false
|
||||
}
|
||||
} else {
|
||||
if let windowScrollY = windowScrollY {
|
||||
_ = try? await webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
|
||||
self.windowScrollY = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WKUIDelegate
|
||||
|
||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
// This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One
|
||||
// example where this is used is in YouTube's embedded video player when a user clicks on the video's title
|
||||
// or on the "Watch in YouTube" button. For our purposes we'll handle such window.open calls the same way we
|
||||
// handle clicks on a URL.
|
||||
if let url = navigationAction.request.url {
|
||||
self.openInBrowser(url, flags: navigationAction.modifierFlags)
|
||||
let flags = navigationAction.modifierFlags
|
||||
|
||||
Task { @MainActor in
|
||||
self.openInBrowser(url, flags: flags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -115,16 +115,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
return sidebarViewController?.selectedObjects
|
||||
}
|
||||
|
||||
func handle(_ response: UNNotificationResponse) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
func handle(articlePathInfo: ArticlePathInfo) {
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathInfo)
|
||||
}
|
||||
|
||||
func handle(_ activity: NSUserActivity) {
|
||||
guard let userInfo = activity.userInfo else { return }
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
|
||||
guard let userInfo = activity.userInfo, let articlePathUserInfo = ArticlePathInfo(userInfo: userInfo) else {
|
||||
return
|
||||
}
|
||||
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct NNW3ImportController {
|
||||
@MainActor struct NNW3ImportController {
|
||||
|
||||
/// Import NNW3 subscriptions if they exist.
|
||||
/// Return true if Subscriptions.plist was found and subscriptions were imported.
|
||||
|
||||
@@ -11,7 +11,7 @@ import RSCore
|
||||
|
||||
// image - title - unreadCount
|
||||
|
||||
struct SidebarCellLayout {
|
||||
@MainActor struct SidebarCellLayout {
|
||||
|
||||
let faviconRect: CGRect
|
||||
let titleRect: CGRect
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
enum SidebarDeleteItemsAlert {
|
||||
@MainActor struct SidebarDeleteItemsAlert {
|
||||
|
||||
/// Builds a delete confirmation dialog for the supplied nodes
|
||||
static func build(_ nodes: [Node]) -> NSAlert {
|
||||
|
||||
@@ -12,7 +12,7 @@ import Articles
|
||||
import RSCore
|
||||
import Account
|
||||
|
||||
@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
|
||||
@objc @MainActor final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
|
||||
|
||||
let treeController: TreeController
|
||||
static let dragOperationNone = NSDragOperation(rawValue: 0)
|
||||
|
||||
@@ -17,13 +17,13 @@ extension Notification.Name {
|
||||
}
|
||||
|
||||
protocol SidebarDelegate: AnyObject {
|
||||
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
||||
func unreadCount(for: AnyObject) -> Int
|
||||
func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
||||
@MainActor func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
||||
@MainActor func unreadCount(for: AnyObject) -> Int
|
||||
@MainActor func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
||||
}
|
||||
|
||||
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
||||
|
||||
@objc @MainActor class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
||||
|
||||
@IBOutlet weak var outlineView: NSOutlineView!
|
||||
|
||||
weak var delegate: SidebarDelegate?
|
||||
@@ -483,9 +483,9 @@ protocol SidebarDelegate: AnyObject {
|
||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||
}
|
||||
|
||||
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
|
||||
guard let accountNode = findAccountNode(userInfo),
|
||||
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
|
||||
func deepLinkRevealAndSelect(for articlePathInfo: ArticlePathInfo) {
|
||||
guard let accountNode = findAccountNode(articlePathInfo),
|
||||
let feedNode = findFeedNode(articlePathInfo, beginningAt: accountNode),
|
||||
let feed = feedNode.representedObject as? SidebarItem else {
|
||||
return
|
||||
}
|
||||
@@ -738,16 +738,17 @@ private extension SidebarViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
|
||||
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
|
||||
func findAccountNode(_ articlePathInfo: ArticlePathInfo) -> Node? {
|
||||
|
||||
guard let accountID = articlePathInfo.accountID else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
||||
return node
|
||||
}
|
||||
|
||||
guard let accountName = userInfo?[ArticlePathKey.accountName] as? String else {
|
||||
guard let accountName = articlePathInfo.accountName else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -758,8 +759,8 @@ private extension SidebarViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
|
||||
guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else {
|
||||
func findFeedNode(_ articlePathInfo: ArticlePathInfo, beginningAt startingNode: Node) -> Node? {
|
||||
guard let feedID = articlePathInfo.feedID else {
|
||||
return nil
|
||||
}
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
class UnreadCountView : NSView {
|
||||
@MainActor final class UnreadCountView : NSView {
|
||||
|
||||
struct Appearance {
|
||||
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
|
||||
|
||||
@@ -11,12 +11,13 @@ import Articles
|
||||
import RSCore
|
||||
|
||||
extension Article: PasteboardWriterOwner {
|
||||
public var pasteboardWriter: NSPasteboardWriting {
|
||||
|
||||
@MainActor public var pasteboardWriter: NSPasteboardWriting {
|
||||
return ArticlePasteboardWriter(article: self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
@objc @MainActor final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
let article: Article
|
||||
static let articleUTI = "com.ranchero.article"
|
||||
|
||||
@@ -26,7 +26,7 @@ struct TextFieldSizeInfo {
|
||||
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
|
||||
}
|
||||
|
||||
final class MultilineTextFieldSizer {
|
||||
@MainActor final class MultilineTextFieldSizer {
|
||||
|
||||
private let numberOfLines: Int
|
||||
private let font: NSFont
|
||||
@@ -35,7 +35,7 @@ final class MultilineTextFieldSizer {
|
||||
private let doubleLineHeightEstimate: Int
|
||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||
private var attributedCache = [NSAttributedString: WidthHeightCache]()
|
||||
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||
@MainActor private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||
|
||||
private init(numberOfLines: Int, font: NSFont) {
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import AppKit
|
||||
// Uses a cache.
|
||||
// Main thready only.
|
||||
|
||||
final class SingleLineTextFieldSizer {
|
||||
@MainActor final class SingleLineTextFieldSizer {
|
||||
|
||||
let font: NSFont
|
||||
private let textField: NSTextField
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Articles
|
||||
|
||||
struct TimelineCellData {
|
||||
@MainActor struct TimelineCellData {
|
||||
|
||||
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import RSCore
|
||||
|
||||
struct TimelineCellLayout {
|
||||
@MainActor struct TimelineCellLayout {
|
||||
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
|
||||
@@ -11,10 +11,10 @@ import Account
|
||||
import Articles
|
||||
|
||||
protocol TimelineContainerViewControllerDelegate: AnyObject {
|
||||
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
||||
func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
|
||||
func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
||||
|
||||
|
||||
@MainActor func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
||||
@MainActor func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
|
||||
@MainActor func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
||||
}
|
||||
|
||||
final class TimelineContainerViewController: NSViewController {
|
||||
|
||||
@@ -13,9 +13,10 @@ import Account
|
||||
import os.log
|
||||
|
||||
protocol TimelineDelegate: AnyObject {
|
||||
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
||||
func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||
|
||||
@MainActor func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||
@MainActor func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
||||
@MainActor func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||
}
|
||||
|
||||
enum TimelineShowFeedName {
|
||||
@@ -535,12 +536,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
func goToDeepLink(for userInfo: [AnyHashable : Any]) {
|
||||
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
|
||||
func goToDeepLink(for articlePathInfo: ArticlePathInfo) {
|
||||
|
||||
guard let articleID = articlePathInfo.articleID else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
if isReadFiltered ?? false {
|
||||
if let accountName = userInfo[ArticlePathKey.accountName] as? String,
|
||||
if let accountName = articlePathInfo.accountName,
|
||||
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||
await fetchAndReplaceArticles()
|
||||
|
||||
Reference in New Issue
Block a user