From c3bcf827134bc7e17f59acace8ed7ea30fb4df20 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Thu, 11 Jan 2018 22:18:46 -0800 Subject: [PATCH 01/27] Make the send-to-Micro.blog command work. Need some tweaking, but it mostly does the job. --- Commands/SendToCommand.swift | 45 +++++++++++++++++-- Commands/SendToMarsEditCommand.swift | 31 ++++++++++++- Commands/SendToMicroBlogCommand.swift | 24 +++++----- Evergreen.xcodeproj/project.pbxproj | 2 + Evergreen/AppDelegate.swift | 4 ++ .../MainWindow/MainWindowController.swift | 25 +++++++++++ .../Timeline/ArticlePasteboardWriter.swift | 2 +- 7 files changed, 115 insertions(+), 18 deletions(-) diff --git a/Commands/SendToCommand.swift b/Commands/SendToCommand.swift index 413ddb8c1..88265a6d2 100644 --- a/Commands/SendToCommand.swift +++ b/Commands/SendToCommand.swift @@ -12,15 +12,54 @@ import Cocoa protocol SendToCommand { + var title: String { get } + var image: NSImage? { get } + func canSendObject(_ object: Any?, selectedText: String?) -> Bool func sendObject(_ object: Any?, selectedText: String?) } -extension SendToCommand { - func appExistsOnDisk(_ bundleIdentifier: String) -> Bool { +final class ApplicationSpecifier { - if let _ = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleIdentifier) { + let bundleID: String + var icon: NSImage? = nil + var existsOnDisk = false + var path: String? = nil + + init(bundleID: String) { + + self.bundleID = bundleID + update() + } + + func update() { + + path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) + if let path = path { + if icon == nil { + icon = NSWorkspace.shared.icon(forFile: path) + } + existsOnDisk = true + } + else { + existsOnDisk = false + icon = nil + } + } + + func launch() -> Bool { + + guard existsOnDisk, let path = path else { + return false + } + + let url = URL(fileURLWithPath: path) + if let runningApplication = try? NSWorkspace.shared.launchApplication(at: url, options: [.withErrorPresentation], configuration: [:]) { + if runningApplication.isFinishedLaunching { + return true + } + sleep(3) // Give the app time to launch. This is ugly. return true } return false diff --git a/Commands/SendToMarsEditCommand.swift b/Commands/SendToMarsEditCommand.swift index 5e221e843..6e3858375 100644 --- a/Commands/SendToMarsEditCommand.swift +++ b/Commands/SendToMarsEditCommand.swift @@ -6,16 +6,45 @@ // Copyright © 2018 Ranchero Software. All rights reserved. // -import Foundation +import Cocoa final class SendToMarsEditCommand: SendToCommand { + let title = NSLocalizedString("Send to MarsEdit", comment: "Send to command") + + var image: NSImage? { + return appSpecifierToUse()?.icon ?? nil + } + + private let marsEditApps = [ApplicationSpecifier(bundleID: "com.red-sweater.marsedit4"), ApplicationSpecifier(bundleID: "com.red-sweater.marsedit")] + func canSendObject(_ object: Any?, selectedText: String?) -> Bool { + if let _ = appSpecifierToUse() { + return true + } return false } func sendObject(_ object: Any?, selectedText: String?) { + if !canSendObject(object, selectedText: selectedText) { + return + } + } +} + +private extension SendToMarsEditCommand { + + func appSpecifierToUse() -> ApplicationSpecifier? { + + for specifier in marsEditApps { + specifier.update() + if specifier.existsOnDisk { + return specifier + } + } + + return nil } } diff --git a/Commands/SendToMicroBlogCommand.swift b/Commands/SendToMicroBlogCommand.swift index 9af95afed..7e1090328 100644 --- a/Commands/SendToMicroBlogCommand.swift +++ b/Commands/SendToMicroBlogCommand.swift @@ -13,18 +13,18 @@ import Data final class SendToMicroBlogCommand: SendToCommand { - private let bundleID = "blog.micro.mac" - private var appExists = false + let title = NSLocalizedString("Send to Micro.blog", comment: "Send to command") - init() { - - self.appExists = appExistsOnDisk(bundleID) - NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: NSApplication.didBecomeActiveNotification, object: nil) + var image: NSImage? { + return microBlogApp.icon } + private let microBlogApp = ApplicationSpecifier(bundleID: "blog.micro.mac") + func canSendObject(_ object: Any?, selectedText: String?) -> Bool { - guard appExists, let article = object as? Article, let _ = article.preferredLink else { + microBlogApp.update() + guard microBlogApp.existsOnDisk, let article = (object as? ArticlePasteboardWriter)?.article, let _ = article.preferredLink else { return false } @@ -36,7 +36,10 @@ final class SendToMicroBlogCommand: SendToCommand { guard canSendObject(object, selectedText: selectedText) else { return } - guard let article = object as? Article else { + guard let article = (object as? ArticlePasteboardWriter)?.article else { + return + } + guard microBlogApp.existsOnDisk, microBlogApp.launch() else { return } @@ -67,11 +70,6 @@ final class SendToMicroBlogCommand: SendToCommand { let _ = try? NSWorkspace.shared.open(url, options: [], configuration: [:]) } - - @objc func appDidBecomeActive(_ note: Notification) { - - self.appExists = appExistsOnDisk(bundleID) - } } diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index a922d870f..31cc7bc63 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -479,6 +479,7 @@ 846E77161F6EF5D000A165E2 /* Database.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Database.xcodeproj; path = Frameworks/Database/Database.xcodeproj; sourceTree = ""; }; 846E77301F6EF5D600A165E2 /* Account.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Account.xcodeproj; path = Frameworks/Account/Account.xcodeproj; sourceTree = ""; }; 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkReadOrUnreadCommand.swift; sourceTree = ""; }; + 847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconDownloader.swift; sourceTree = ""; }; 849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderWindowController.swift; sourceTree = ""; }; 849A97511ED9EAC0007D329B /* AddFeedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedController.swift; path = AddFeed/AddFeedController.swift; sourceTree = ""; }; @@ -1094,6 +1095,7 @@ 84FB9A2C1EDCD6A4003D53B9 /* Frameworks */ = { isa = PBXGroup; children = ( + 847752FE2008879500D93690 /* CoreServices.framework */, 84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */, ); name = Frameworks; diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index 068e2b3d7..3f8fe5e95 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -33,6 +33,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return image }() + lazy var sendToCommands: [SendToCommand] = { + return [SendToMicroBlogCommand()] //, SendToMarsEditCommand()] + }() + var unreadCount = 0 { didSet { if unreadCount != oldValue { diff --git a/Evergreen/MainWindow/MainWindowController.swift b/Evergreen/MainWindow/MainWindowController.swift index 2587f5cdc..f314812d5 100644 --- a/Evergreen/MainWindow/MainWindowController.swift +++ b/Evergreen/MainWindow/MainWindowController.swift @@ -287,6 +287,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { let items = selectedArticles.map { ArticlePasteboardWriter(article: $0) } let sharingServicePicker = NSSharingServicePicker(items: items) + sharingServicePicker.delegate = self sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY) } @@ -299,6 +300,30 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } +// MARK: - NSSharingServicePickerDelegate + +extension MainWindowController: NSSharingServicePickerDelegate { + + func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { + + let sendToServices = appDelegate.sendToCommands.flatMap { (sendToCommand) -> NSSharingService? in + + guard let object = items.first else { + return nil + } + guard sendToCommand.canSendObject(object, selectedText: nil) else { + return nil + } + + let image = sendToCommand.image ?? appDelegate.genericFeedImage ?? NSImage() + return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) { + sendToCommand.sendObject(object, selectedText: nil) + } + } + return proposedServices + sendToServices + } +} + // MARK: - NSToolbarDelegate extension NSToolbarItem.Identifier { diff --git a/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift index 67ed7a540..f49afe0b4 100644 --- a/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -11,7 +11,7 @@ import Data @objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting { - private let article: Article + let article: Article static let articleUTI = "com.ranchero.article" static let articleUTIType = NSPasteboard.PasteboardType(rawValue: articleUTI) static let articleUTIInternal = "com.ranchero.evergreen.internal.article" From f430d6a095cee468fbfd2adfd10506adadc64181 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 13 Jan 2018 17:40:27 -0800 Subject: [PATCH 02/27] Add Dictionary and String extensions for creating URL query strings. Add tests. --- .../RSWeb/RSWeb.xcodeproj/project.pbxproj | 16 +++++++ Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift | 46 +++++++++++++++++++ Frameworks/RSWeb/RSWeb/String+RSWeb.swift | 21 +++++++++ .../RSWeb/RSWebTests/DictionaryTests.swift | 45 ++++++++++++++++++ Frameworks/RSWeb/RSWebTests/StringTests.swift | 24 ++++++++++ 5 files changed, 152 insertions(+) create mode 100644 Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift create mode 100644 Frameworks/RSWeb/RSWeb/String+RSWeb.swift create mode 100644 Frameworks/RSWeb/RSWebTests/DictionaryTests.swift create mode 100644 Frameworks/RSWeb/RSWebTests/StringTests.swift diff --git a/Frameworks/RSWeb/RSWeb.xcodeproj/project.pbxproj b/Frameworks/RSWeb/RSWeb.xcodeproj/project.pbxproj index 1d0c96a5b..a88e9cbe1 100755 --- a/Frameworks/RSWeb/RSWeb.xcodeproj/project.pbxproj +++ b/Frameworks/RSWeb/RSWeb.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 8409DB2C200AE4D700CE879E /* Dictionary+RSWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409DB2B200AE4D700CE879E /* Dictionary+RSWeb.swift */; }; + 8409DB2E200AE74400CE879E /* DictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409DB2D200AE74400CE879E /* DictionaryTests.swift */; }; + 8409DB30200AE81400CE879E /* String+RSWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409DB2F200AE81400CE879E /* String+RSWeb.swift */; }; 84245C5A1FDC690A0074AFBB /* WebServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C591FDC690A0074AFBB /* WebServiceProvider.swift */; }; 84245C5B1FDC690A0074AFBB /* WebServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C591FDC690A0074AFBB /* WebServiceProvider.swift */; }; 84245C5D1FDC697A0074AFBB /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C5C1FDC697A0074AFBB /* Credentials.swift */; }; @@ -15,6 +18,7 @@ 84245C611FDC69F20074AFBB /* APICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C5F1FDC69F20074AFBB /* APICall.swift */; }; 84245C6F1FDDCD8C0074AFBB /* HTTPResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C6E1FDDCD8C0074AFBB /* HTTPResult.swift */; }; 84245C701FDDCD8C0074AFBB /* HTTPResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C6E1FDDCD8C0074AFBB /* HTTPResult.swift */; }; + 84261183200AE918004D89DD /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84261182200AE918004D89DD /* StringTests.swift */; }; 842ED2E71E12FB8A000CF738 /* HTTPRequestHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842ED2E61E12FB8A000CF738 /* HTTPRequestHeader.swift */; }; 842ED2E81E12FB8A000CF738 /* HTTPRequestHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842ED2E61E12FB8A000CF738 /* HTTPRequestHeader.swift */; }; 842ED2EA1E12FB91000CF738 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842ED2E91E12FB91000CF738 /* HTTPMethod.swift */; }; @@ -61,10 +65,14 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 8409DB2B200AE4D700CE879E /* Dictionary+RSWeb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Dictionary+RSWeb.swift"; path = "RSWeb/Dictionary+RSWeb.swift"; sourceTree = ""; }; + 8409DB2D200AE74400CE879E /* DictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryTests.swift; sourceTree = ""; }; + 8409DB2F200AE81400CE879E /* String+RSWeb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "String+RSWeb.swift"; path = "RSWeb/String+RSWeb.swift"; sourceTree = ""; }; 84245C591FDC690A0074AFBB /* WebServiceProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebServiceProvider.swift; sourceTree = ""; }; 84245C5C1FDC697A0074AFBB /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Credentials.swift; path = RSWeb/Credentials.swift; sourceTree = ""; }; 84245C5F1FDC69F20074AFBB /* APICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICall.swift; sourceTree = ""; }; 84245C6E1FDDCD8C0074AFBB /* HTTPResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPResult.swift; path = RSWeb/HTTPResult.swift; sourceTree = ""; }; + 84261182200AE918004D89DD /* StringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = ""; }; 842ED2E61E12FB8A000CF738 /* HTTPRequestHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPRequestHeader.swift; path = RSWeb/HTTPRequestHeader.swift; sourceTree = ""; }; 842ED2E91E12FB91000CF738 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = RSWeb/HTTPMethod.swift; sourceTree = ""; }; 842ED2EC1E12FB97000CF738 /* HTTPResponseCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPResponseCode.swift; path = RSWeb/HTTPResponseCode.swift; sourceTree = ""; }; @@ -143,6 +151,8 @@ 842ED2D41E11FE8B000CF738 /* Constants */, 849C09231E0CAD67006B03FA /* Downloading */, 842ED30A1E12FBD8000CF738 /* URL+RSWeb.swift */, + 8409DB2B200AE4D700CE879E /* Dictionary+RSWeb.swift */, + 8409DB2F200AE81400CE879E /* String+RSWeb.swift */, 842ED3131E12FBE7000CF738 /* MimeType.swift */, 842ED3101E12FBE1000CF738 /* MacWebBrowser.swift */, 84245C5C1FDC697A0074AFBB /* Credentials.swift */, @@ -177,6 +187,8 @@ isa = PBXGroup; children = ( 849C08C41E0CAC86006B03FA /* RSWebTests.swift */, + 8409DB2D200AE74400CE879E /* DictionaryTests.swift */, + 84261182200AE918004D89DD /* StringTests.swift */, 849C08C61E0CAC86006B03FA /* Info.plist */, ); path = RSWebTests; @@ -367,6 +379,7 @@ 842ED3081E12FBD2000CF738 /* URLRequest+RSWeb.swift in Sources */, 842ED3051E12FBCC000CF738 /* NSMutableURLRequest+RSWeb.swift in Sources */, 842ED2E71E12FB8A000CF738 /* HTTPRequestHeader.swift in Sources */, + 8409DB30200AE81400CE879E /* String+RSWeb.swift in Sources */, 842ED3111E12FBE1000CF738 /* MacWebBrowser.swift in Sources */, 842ED3141E12FBE7000CF738 /* MimeType.swift in Sources */, 84245C5D1FDC697A0074AFBB /* Credentials.swift in Sources */, @@ -376,6 +389,7 @@ 842ED2F91E12FBB5000CF738 /* DownloadProgress.swift in Sources */, 842ED2EA1E12FB91000CF738 /* HTTPMethod.swift in Sources */, 842ED3021E12FBC7000CF738 /* HTTPConditionalGetInfo.swift in Sources */, + 8409DB2C200AE4D700CE879E /* Dictionary+RSWeb.swift in Sources */, 842ED2ED1E12FB97000CF738 /* HTTPResponseCode.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -385,6 +399,8 @@ buildActionMask = 2147483647; files = ( 849C08C51E0CAC86006B03FA /* RSWebTests.swift in Sources */, + 84261183200AE918004D89DD /* StringTests.swift in Sources */, + 8409DB2E200AE74400CE879E /* DictionaryTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift b/Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift new file mode 100644 index 000000000..db2bddc91 --- /dev/null +++ b/Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift @@ -0,0 +1,46 @@ +// +// Dictionary+RSWeb.swift +// RSWeb +// +// Created by Brent Simmons on 1/13/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import Foundation + +public extension Dictionary { + + public func urlQueryString() -> String? { + + // Turn a dictionary into string like foo=bar¶m2=some+thing + // Return nil if empty dictionary. + + if isEmpty { + return nil + } + + var s = "" + var numberAdded = 0 + for (key, value) in self { + + guard let key = key as? String, let value = value as? String else { + continue + } + guard let encodedKey = key.encodedForURLQuery(), let encodedValue = value.encodedForURLQuery() else { + continue + } + + if numberAdded > 0 { + s += "&" + } + s += "\(encodedKey)=\(encodedValue)" + numberAdded += 1 + } + + if numberAdded < 1 { + return nil + } + + return s + } +} diff --git a/Frameworks/RSWeb/RSWeb/String+RSWeb.swift b/Frameworks/RSWeb/RSWeb/String+RSWeb.swift new file mode 100644 index 000000000..a8e17b29d --- /dev/null +++ b/Frameworks/RSWeb/RSWeb/String+RSWeb.swift @@ -0,0 +1,21 @@ +// +// String+RSWeb.swift +// RSWeb +// +// Created by Brent Simmons on 1/13/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import Foundation + +public extension String { + + public func encodedForURLQuery() -> String? { + + let s = replacingOccurrences(of: " ", with: "+") + guard let encodedString = s.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + return nil + } + return encodedString.replacingOccurrences(of: "&", with: "%38") + } +} diff --git a/Frameworks/RSWeb/RSWebTests/DictionaryTests.swift b/Frameworks/RSWeb/RSWebTests/DictionaryTests.swift new file mode 100644 index 000000000..01a86af78 --- /dev/null +++ b/Frameworks/RSWeb/RSWebTests/DictionaryTests.swift @@ -0,0 +1,45 @@ +// +// DictionaryTests.swift +// RSWebTests +// +// Created by Brent Simmons on 1/13/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import XCTest + +class DictionaryTests: XCTestCase { + + func testSimpleQueryString() { + + let d = ["foo": "bar", "param1": "This is a value."] + let s = d.urlQueryString() + + XCTAssertTrue(s == "foo=bar¶m1=This+is+a+value." || s == "param1=This+is+a+value.&foo=bar") + } + + func testQueryStringWithAmpersand() { + + let d = ["fo&o": "bar", "param1": "This is a&value."] + let s = d.urlQueryString() + + XCTAssertTrue(s == "fo%38o=bar¶m1=This+is+a%38value." || s == "param1=This+is+a%38value.&fo%38o=bar") + } + + func testQueryStringWithAccentedCharacters() { + + let d = ["fée": "bør"] + let s = d.urlQueryString() + + XCTAssertTrue(s == "f%C3%A9e=b%C3%B8r") + } + + func testQueryStringWithEmoji() { + + let d = ["🌴e": "bar🎩🌴"] + let s = d.urlQueryString() + + XCTAssertTrue(s == "%F0%9F%8C%B4e=bar%F0%9F%8E%A9%F0%9F%8C%B4") + } + +} diff --git a/Frameworks/RSWeb/RSWebTests/StringTests.swift b/Frameworks/RSWeb/RSWebTests/StringTests.swift new file mode 100644 index 000000000..9a8a9b7b0 --- /dev/null +++ b/Frameworks/RSWeb/RSWebTests/StringTests.swift @@ -0,0 +1,24 @@ +// +// StringTests.swift +// RSWebTests +// +// Created by Brent Simmons on 1/13/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import XCTest + +class StringTests: XCTestCase { + + func testURLQueryEncoding() { + + var s = "foo".encodedForURLQuery() + XCTAssertEqual(s, "foo") + + s = "foo bar".encodedForURLQuery() + XCTAssertEqual(s, "foo+bar") + + s = "foo bar &well".encodedForURLQuery() + XCTAssertEqual(s, "foo+bar+%38well") + } +} From 8df34bfcda8e646276fc360c534d11b32687741f Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 09:36:24 -0800 Subject: [PATCH 03/27] Use new urlQueryString method from RSWeb. --- Commands/SendToMicroBlogCommand.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Commands/SendToMicroBlogCommand.swift b/Commands/SendToMicroBlogCommand.swift index 7e1090328..00f5e9d27 100644 --- a/Commands/SendToMicroBlogCommand.swift +++ b/Commands/SendToMicroBlogCommand.swift @@ -61,10 +61,11 @@ final class SendToMicroBlogCommand: SendToCommand { s = link } - guard let encodedString = s.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + let urlQueryDictionary = ["text": s] + guard let urlQueryString = urlQueryDictionary.urlQueryString() else { return } - guard let url = URL(string: "microblog://post?text=" + encodedString) else { + guard let url = URL(string: "microblog://post?" + urlQueryString) else { return } From fd7c6d07aca4fe5d0960b7d0df6704db7ef7ea21 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 10:56:06 -0800 Subject: [PATCH 04/27] Add UserApp class to RSCore. It represents an of the type usually found in /Applications. A UserApp may or may not be running and may or may not exist locally on disk. It could be entirely fictional, even. --- .../RSCore/RSCore.xcodeproj/project.pbxproj | 4 + Frameworks/RSCore/RSCore/AppKit/UserApp.swift | 123 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 Frameworks/RSCore/RSCore/AppKit/UserApp.swift diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index 0c39af914..8e6e5309b 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ 842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CB1ED623C7000A8B52 /* UniqueIdentifier.swift */; }; 8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432B1851DACA0E90057D6DF /* NSResponder-Extensions.swift */; }; 8432B1881DACA2060057D6DF /* NSWindow-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432B1871DACA2060057D6DF /* NSWindow-Extensions.swift */; }; + 8434D15C200BD6F400D6281E /* UserApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8434D15B200BD6F400D6281E /* UserApp.swift */; }; 84411E731FE5FFC3004B527F /* NSImage+RSCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E721FE5FFC3004B527F /* NSImage+RSCore.swift */; }; 844B5B571FE9D36000C7C76A /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B561FE9D36000C7C76A /* Keyboard.swift */; }; 844C915B1B65753E0051FC1B /* RSPlist.h in Headers */ = {isa = PBXBuildFile; fileRef = 844C91591B65753E0051FC1B /* RSPlist.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -191,6 +192,7 @@ 842E45CB1ED623C7000A8B52 /* UniqueIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniqueIdentifier.swift; sourceTree = ""; }; 8432B1851DACA0E90057D6DF /* NSResponder-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSResponder-Extensions.swift"; sourceTree = ""; }; 8432B1871DACA2060057D6DF /* NSWindow-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSWindow-Extensions.swift"; sourceTree = ""; }; + 8434D15B200BD6F400D6281E /* UserApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserApp.swift; path = AppKit/UserApp.swift; sourceTree = ""; }; 84411E721FE5FFC3004B527F /* NSImage+RSCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSImage+RSCore.swift"; path = "Images/NSImage+RSCore.swift"; sourceTree = ""; }; 844B5B561FE9D36000C7C76A /* Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Keyboard.swift; path = RSCore/Keyboard.swift; sourceTree = ""; }; 844C91591B65753E0051FC1B /* RSPlist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSPlist.h; path = RSCore/RSPlist.h; sourceTree = ""; }; @@ -460,6 +462,7 @@ 84C687311FBAA3DF00345C9E /* LogWindowController.swift */, 84C687341FBC025600345C9E /* Log.swift */, 84C687371FBC028900345C9E /* LogItem.swift */, + 8434D15B200BD6F400D6281E /* UserApp.swift */, 842DD7F91E1499FA00E061EB /* Views */, ); name = AppKit; @@ -794,6 +797,7 @@ 842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */, 84A8358A1D4EC7B80004C598 /* PlistProviderProtocol.swift in Sources */, 849A339E1AC90A0A0015BA09 /* NSTableView+RSCore.m in Sources */, + 8434D15C200BD6F400D6281E /* UserApp.swift in Sources */, 84CFF51B1AC3C77500CEA6C8 /* RSPlatform.m in Sources */, 845A291F1FC8BC49007B49E3 /* BinaryDiskCache.swift in Sources */, 84CFF52C1AC3CA9700CEA6C8 /* NSData+RSCore.m in Sources */, diff --git a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift new file mode 100644 index 000000000..2f10936d5 --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift @@ -0,0 +1,123 @@ +// +// UserApp.swift +// RSCore +// +// Created by Brent Simmons on 1/14/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import Cocoa + +// Represents an app (the type of app mostly found in /Applications.) +// The app may or may not be running. It may or may not exist. + +final class UserApp { + + let bundleID: String + var icon: NSImage? = nil + var existsOnDisk = false + var path: String? = nil + var runningApplication: NSRunningApplication? = nil + + var isRunning: Bool { + + updateStatus() + if let runningApplication = runningApplication { + return !runningApplication.isTerminated + } + return false + } + + init(bundleID: String) { + + self.bundleID = bundleID + updateStatus() + } + + func updateStatus() { + + if let runningApplication = runningApplication, runningApplication.isTerminated { + self.runningApplication = nil + } + + let runningApplications = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) + for app in runningApplications { + if let runningApplication = runningApplication { + if app == runningApplication { + break + } + } + else { + if !app.isTerminated { + runningApplication = app + break + } + } + } + + if let runningApplication = runningApplication { + existsOnDisk = true + icon = runningApplication.icon + if let bundleURL = runningApplication.bundleURL { + path = bundleURL.path + } + else { + path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) + } + if icon == nil, let path = path { + icon = NSWorkspace.shared.icon(forFile: path) + } + return + } + + path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) + if let path = path { + if icon == nil { + icon = NSWorkspace.shared.icon(forFile: path) + } + existsOnDisk = true + } + else { + existsOnDisk = false + icon = nil + } + } + + func launchIfNeeded() -> Bool { + + // Return true if already running. + // Return true if not running and successfully gets launched. + + updateStatus() + if isRunning { + return true + } + + guard existsOnDisk, let path = path else { + return false + } + + let url = URL(fileURLWithPath: path) + if let app = try? NSWorkspace.shared.launchApplication(at: url, options: [.withErrorPresentation], configuration: [:]) { + runningApplication = app + if app.isFinishedLaunching { + return true + } + Thread.sleep(forTimeInterval: 0.5) // Give the app time to launch. This is ugly. + return true + } + + return false + } + + func bringToFront() -> Bool { + + // Activates the app, ignoring other apps. + // Does not automatically launch the app first. + + updateStatus() + return runningApplication?.activate(options: [.activateIgnoringOtherApps]) ?? false + } +} + + From bbf2b8f130d5cbb7000963056471684d840a8fa4 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 10:56:49 -0800 Subject: [PATCH 05/27] Switch from ApplicationSpecifier to UserApp. --- Commands/SendToCommand.swift | 46 --------------------------- Commands/SendToMicroBlogCommand.swift | 7 ++-- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/Commands/SendToCommand.swift b/Commands/SendToCommand.swift index 88265a6d2..e7e13368f 100644 --- a/Commands/SendToCommand.swift +++ b/Commands/SendToCommand.swift @@ -19,49 +19,3 @@ protocol SendToCommand { func sendObject(_ object: Any?, selectedText: String?) } - -final class ApplicationSpecifier { - - let bundleID: String - var icon: NSImage? = nil - var existsOnDisk = false - var path: String? = nil - - init(bundleID: String) { - - self.bundleID = bundleID - update() - } - - func update() { - - path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) - if let path = path { - if icon == nil { - icon = NSWorkspace.shared.icon(forFile: path) - } - existsOnDisk = true - } - else { - existsOnDisk = false - icon = nil - } - } - - func launch() -> Bool { - - guard existsOnDisk, let path = path else { - return false - } - - let url = URL(fileURLWithPath: path) - if let runningApplication = try? NSWorkspace.shared.launchApplication(at: url, options: [.withErrorPresentation], configuration: [:]) { - if runningApplication.isFinishedLaunching { - return true - } - sleep(3) // Give the app time to launch. This is ugly. - return true - } - return false - } -} diff --git a/Commands/SendToMicroBlogCommand.swift b/Commands/SendToMicroBlogCommand.swift index 00f5e9d27..4fcc7b921 100644 --- a/Commands/SendToMicroBlogCommand.swift +++ b/Commands/SendToMicroBlogCommand.swift @@ -8,6 +8,7 @@ import Cocoa import Data +import RSCore // Not undoable. @@ -19,11 +20,11 @@ final class SendToMicroBlogCommand: SendToCommand { return microBlogApp.icon } - private let microBlogApp = ApplicationSpecifier(bundleID: "blog.micro.mac") + private let microBlogApp = UserApp(bundleID: "blog.micro.mac") func canSendObject(_ object: Any?, selectedText: String?) -> Bool { - microBlogApp.update() + microBlogApp.updateStatus() guard microBlogApp.existsOnDisk, let article = (object as? ArticlePasteboardWriter)?.article, let _ = article.preferredLink else { return false } @@ -39,7 +40,7 @@ final class SendToMicroBlogCommand: SendToCommand { guard let article = (object as? ArticlePasteboardWriter)?.article else { return } - guard microBlogApp.existsOnDisk, microBlogApp.launch() else { + guard microBlogApp.launchIfNeeded(), microBlogApp.bringToFront() else { return } From 75d0752a677296651985d55dc722c40645ff5aa9 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 11:00:29 -0800 Subject: [PATCH 06/27] Make UserApp properties and methods public. --- Frameworks/RSCore/RSCore/AppKit/UserApp.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift index 2f10936d5..58c21468d 100644 --- a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift +++ b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift @@ -11,15 +11,15 @@ import Cocoa // Represents an app (the type of app mostly found in /Applications.) // The app may or may not be running. It may or may not exist. -final class UserApp { +public final class UserApp { - let bundleID: String - var icon: NSImage? = nil - var existsOnDisk = false - var path: String? = nil - var runningApplication: NSRunningApplication? = nil + public let bundleID: String + public var icon: NSImage? = nil + public var existsOnDisk = false + public var path: String? = nil + public var runningApplication: NSRunningApplication? = nil - var isRunning: Bool { + public var isRunning: Bool { updateStatus() if let runningApplication = runningApplication { @@ -28,13 +28,13 @@ final class UserApp { return false } - init(bundleID: String) { + public init(bundleID: String) { self.bundleID = bundleID updateStatus() } - func updateStatus() { + public func updateStatus() { if let runningApplication = runningApplication, runningApplication.isTerminated { self.runningApplication = nil @@ -83,7 +83,7 @@ final class UserApp { } } - func launchIfNeeded() -> Bool { + public func launchIfNeeded() -> Bool { // Return true if already running. // Return true if not running and successfully gets launched. @@ -110,7 +110,7 @@ final class UserApp { return false } - func bringToFront() -> Bool { + public func bringToFront() -> Bool { // Activates the app, ignoring other apps. // Does not automatically launch the app first. From b05d2f8f5fa9b1e49d90a84bc7fbdf8716a008e0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 11:00:42 -0800 Subject: [PATCH 07/27] User UserApp with SendToMarsEditCommand. --- Commands/SendToMarsEditCommand.swift | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Commands/SendToMarsEditCommand.swift b/Commands/SendToMarsEditCommand.swift index 6e3858375..ea6b8be83 100644 --- a/Commands/SendToMarsEditCommand.swift +++ b/Commands/SendToMarsEditCommand.swift @@ -7,20 +7,21 @@ // import Cocoa +import RSCore final class SendToMarsEditCommand: SendToCommand { let title = NSLocalizedString("Send to MarsEdit", comment: "Send to command") var image: NSImage? { - return appSpecifierToUse()?.icon ?? nil + return appToUse()?.icon ?? nil } - private let marsEditApps = [ApplicationSpecifier(bundleID: "com.red-sweater.marsedit4"), ApplicationSpecifier(bundleID: "com.red-sweater.marsedit")] + private let marsEditApps = [UserApp(bundleID: "com.red-sweater.marsedit4"), UserApp(bundleID: "com.red-sweater.marsedit")] func canSendObject(_ object: Any?, selectedText: String?) -> Bool { - if let _ = appSpecifierToUse() { + if let _ = appToUse() { return true } return false @@ -36,12 +37,15 @@ final class SendToMarsEditCommand: SendToCommand { private extension SendToMarsEditCommand { - func appSpecifierToUse() -> ApplicationSpecifier? { + func appToUse() -> UserApp? { - for specifier in marsEditApps { - specifier.update() - if specifier.existsOnDisk { - return specifier + for app in marsEditApps { + app.updateStatus() + if app.isRunning { + return app + } + if app.existsOnDisk { + return app } } From 39d6086e0cbf57cb6e97ea7d572c3f836774778a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 11:11:53 -0800 Subject: [PATCH 08/27] Use %20 instead of + when encoding for URL query strings, since it appears to be more compatible. (Well, it works better with Micro.blog.) --- Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift | 2 +- Frameworks/RSWeb/RSWeb/String+RSWeb.swift | 3 +-- Frameworks/RSWeb/RSWebTests/DictionaryTests.swift | 4 ++-- Frameworks/RSWeb/RSWebTests/StringTests.swift | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift b/Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift index db2bddc91..99dbd87a7 100644 --- a/Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift +++ b/Frameworks/RSWeb/RSWeb/Dictionary+RSWeb.swift @@ -12,7 +12,7 @@ public extension Dictionary { public func urlQueryString() -> String? { - // Turn a dictionary into string like foo=bar¶m2=some+thing + // Turn a dictionary into string like foo=bar¶m2=some%20thing // Return nil if empty dictionary. if isEmpty { diff --git a/Frameworks/RSWeb/RSWeb/String+RSWeb.swift b/Frameworks/RSWeb/RSWeb/String+RSWeb.swift index a8e17b29d..7fa68e816 100644 --- a/Frameworks/RSWeb/RSWeb/String+RSWeb.swift +++ b/Frameworks/RSWeb/RSWeb/String+RSWeb.swift @@ -12,8 +12,7 @@ public extension String { public func encodedForURLQuery() -> String? { - let s = replacingOccurrences(of: " ", with: "+") - guard let encodedString = s.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + guard let encodedString = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil } return encodedString.replacingOccurrences(of: "&", with: "%38") diff --git a/Frameworks/RSWeb/RSWebTests/DictionaryTests.swift b/Frameworks/RSWeb/RSWebTests/DictionaryTests.swift index 01a86af78..12e692684 100644 --- a/Frameworks/RSWeb/RSWebTests/DictionaryTests.swift +++ b/Frameworks/RSWeb/RSWebTests/DictionaryTests.swift @@ -15,7 +15,7 @@ class DictionaryTests: XCTestCase { let d = ["foo": "bar", "param1": "This is a value."] let s = d.urlQueryString() - XCTAssertTrue(s == "foo=bar¶m1=This+is+a+value." || s == "param1=This+is+a+value.&foo=bar") + XCTAssertTrue(s == "foo=bar¶m1=This%20is%20a%20value." || s == "param1=This%20is%20a%20value.&foo=bar") } func testQueryStringWithAmpersand() { @@ -23,7 +23,7 @@ class DictionaryTests: XCTestCase { let d = ["fo&o": "bar", "param1": "This is a&value."] let s = d.urlQueryString() - XCTAssertTrue(s == "fo%38o=bar¶m1=This+is+a%38value." || s == "param1=This+is+a%38value.&fo%38o=bar") + XCTAssertTrue(s == "fo%38o=bar¶m1=This%20is%20a%38value." || s == "param1=This%20is%20a%38value.&fo%38o=bar") } func testQueryStringWithAccentedCharacters() { diff --git a/Frameworks/RSWeb/RSWebTests/StringTests.swift b/Frameworks/RSWeb/RSWebTests/StringTests.swift index 9a8a9b7b0..9bee18696 100644 --- a/Frameworks/RSWeb/RSWebTests/StringTests.swift +++ b/Frameworks/RSWeb/RSWebTests/StringTests.swift @@ -16,9 +16,9 @@ class StringTests: XCTestCase { XCTAssertEqual(s, "foo") s = "foo bar".encodedForURLQuery() - XCTAssertEqual(s, "foo+bar") + XCTAssertEqual(s, "foo%20bar") s = "foo bar &well".encodedForURLQuery() - XCTAssertEqual(s, "foo+bar+%38well") + XCTAssertEqual(s, "foo%20bar%20%38well") } } From f9c5c5ad60bc4b0779fb6094878527e9a065453a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 11:12:28 -0800 Subject: [PATCH 09/27] Fix logic in SendToMarsEditCommand where it finds the app to talk to. A running app takes precedence. --- Commands/SendToMarsEditCommand.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Commands/SendToMarsEditCommand.swift b/Commands/SendToMarsEditCommand.swift index ea6b8be83..aded2f4e9 100644 --- a/Commands/SendToMarsEditCommand.swift +++ b/Commands/SendToMarsEditCommand.swift @@ -39,11 +39,15 @@ private extension SendToMarsEditCommand { func appToUse() -> UserApp? { + marsEditApps.forEach{ $0.updateStatus() } + for app in marsEditApps { - app.updateStatus() if app.isRunning { return app } + } + + for app in marsEditApps { if app.existsOnDisk { return app } From 7a8e0ec4aa6108b22f3e44ace87ca3eb5f4463fa Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 11:19:19 -0800 Subject: [PATCH 10/27] =?UTF-8?q?Increase=20the=20sleep=20interval=20for?= =?UTF-8?q?=20waiting=20for=20an=20app=20to=20launch.=20I=E2=80=99m=20sure?= =?UTF-8?q?=20there=E2=80=99s=20a=20better=20way=20to=20do=20this,=20but?= =?UTF-8?q?=20I=20don=E2=80=99t=20know=20what=20it=20is=20yet.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frameworks/RSCore/RSCore/AppKit/UserApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift index 58c21468d..00a71c55e 100644 --- a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift +++ b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift @@ -103,7 +103,7 @@ public final class UserApp { if app.isFinishedLaunching { return true } - Thread.sleep(forTimeInterval: 0.5) // Give the app time to launch. This is ugly. + Thread.sleep(forTimeInterval: 1.0) // Give the app time to launch. This is ugly. return true } From 87aec0b563fc4e0394b7f0097f285eeaddeea9c9 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 12:00:09 -0800 Subject: [PATCH 11/27] Add attribution when posting to Micro.blog. --- Commands/SendToMicroBlogCommand.swift | 51 ++++++++++++++++++--------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/Commands/SendToMicroBlogCommand.swift b/Commands/SendToMicroBlogCommand.swift index 4fcc7b921..a0d8662d2 100644 --- a/Commands/SendToMicroBlogCommand.swift +++ b/Commands/SendToMicroBlogCommand.swift @@ -45,22 +45,9 @@ final class SendToMicroBlogCommand: SendToCommand { } // TODO: get text from contentHTML or contentText if no title and no selectedText. - var s = "" - if let selectedText = selectedText { - s += selectedText - if let link = article.preferredLink { - s += "\n\n\(link)" - } - } - else if let title = article.title { - s += title - if let link = article.preferredLink { - s = "[" + s + "](" + link + ")" - } - } - else if let link = article.preferredLink { - s = link - } + // TODO: consider selectedText. + + let s = article.attributionString + article.linkString let urlQueryDictionary = ["text": s] guard let urlQueryString = urlQueryDictionary.urlQueryString() else { @@ -74,4 +61,36 @@ final class SendToMicroBlogCommand: SendToCommand { } } +private extension Article { + var attributionString: String { + + // Feed name, or feed name + author name (if author is specified per-article). + // Includes trailing space. + + if let feedName = feed?.nameForDisplay, let authorName = authors?.first?.name { + return feedName + ", " + authorName + ": " + } + if let feedName = feed?.nameForDisplay { + return feedName + ": " + } + return "" + } + + var linkString: String { + + // Title + link or just title (if no link) or just link if no title + + if let title = title, let link = preferredLink { + return "[" + title + "](" + link + ")" + } + if let preferredLink = preferredLink { + return preferredLink + } + if let title = title { + return title + } + return "" + } + +} From 4f41824b730125141fb16ec37e1fe4b159977e48 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 12:00:35 -0800 Subject: [PATCH 12/27] Bump version. --- Evergreen/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index 6a7a1b995..7d05afa13 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0d31 + 1.0d32 CFBundleVersion 522 LSMinimumSystemVersion From 2ba1794122f9c334942bdc735417a6434182a463 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 12:13:25 -0800 Subject: [PATCH 13/27] Change send-to command titles to just reflect the app name. --- Commands/SendToMarsEditCommand.swift | 2 +- Commands/SendToMicroBlogCommand.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/SendToMarsEditCommand.swift b/Commands/SendToMarsEditCommand.swift index aded2f4e9..9fc081ddf 100644 --- a/Commands/SendToMarsEditCommand.swift +++ b/Commands/SendToMarsEditCommand.swift @@ -11,7 +11,7 @@ import RSCore final class SendToMarsEditCommand: SendToCommand { - let title = NSLocalizedString("Send to MarsEdit", comment: "Send to command") + let title = "MarsEdit" var image: NSImage? { return appToUse()?.icon ?? nil diff --git a/Commands/SendToMicroBlogCommand.swift b/Commands/SendToMicroBlogCommand.swift index a0d8662d2..d3d710448 100644 --- a/Commands/SendToMicroBlogCommand.swift +++ b/Commands/SendToMicroBlogCommand.swift @@ -14,7 +14,7 @@ import RSCore final class SendToMicroBlogCommand: SendToCommand { - let title = NSLocalizedString("Send to Micro.blog", comment: "Send to command") + let title = "Micro.blog" var image: NSImage? { return microBlogApp.icon From 62d8701ebb018287bfe2010d6e5be89bd7100193 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 14 Jan 2018 12:13:50 -0800 Subject: [PATCH 14/27] Update appcast for 1.0d32. --- Appcasts/evergreen-beta.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index e0208b2da..c659db0e5 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,25 @@ Most recent Evergreen changes with links to updates. en + + Evergreen 1.0d32 + Send to Micro.blog +

See the Sharing command in the toolbar for a new feature: Send to Micro.blog. This sends the current item to your copy the Micro.blog Mac app (if you have it). It gets launched if necessary.

+

This is hugely important, because feed reading isn’t just about reading — it’s also about posting. While Evergreen doesn’t itself include a way to post to the web, other apps do, and Evergreen should make it easy to connect to these other apps.

+

Note: send-to-MarsEdit is next, probably in the next release.

+ +

Misc.

+

Improve the promptness and reliability of feed icons and favicons appearing in the timeline (when a folder is selected).

+

Increase the indentation in the source list so that feeds inside folders line up better.

+ + ]]>
+ Sun, 14 Jan 2018 12:00:00 -0800 + + 10.13 +
+ Version 1.0d31 Date: Sun, 14 Jan 2018 12:24:18 -0800 Subject: [PATCH 15/27] Update appcast again for 1.0d32. --- Appcasts/evergreen-beta.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index c659db0e5..efef9c912 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -11,7 +11,7 @@ Send to Micro.blog -

See the Sharing command in the toolbar for a new feature: Send to Micro.blog. This sends the current item to your copy the Micro.blog Mac app (if you have it). It gets launched if necessary.

+

See the Sharing command in the toolbar for a new feature: send to Micro.blog. This sends the current item to your copy of the Micro.blog Mac app (if you have it). It gets launched if necessary. You can edit the post in Micro.blog before actually posting it to your microblog.

This is hugely important, because feed reading isn’t just about reading — it’s also about posting. While Evergreen doesn’t itself include a way to post to the web, other apps do, and Evergreen should make it easy to connect to these other apps.

Note: send-to-MarsEdit is next, probably in the next release.

@@ -21,7 +21,7 @@ ]]>
Sun, 14 Jan 2018 12:00:00 -0800 - + 10.13
From eaeb333120b1297993be3f73ba3fd4bd141e3084 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 12:10:31 -0800 Subject: [PATCH 16/27] Add NSAppleEventDescriptor category method: +descriptorWithRunningApplication:. --- .../RSCore/RSCore.xcodeproj/project.pbxproj | 18 +++++++++++++-- .../AppKit/NSAppleEventDescriptor+RSCore.h | 19 +++++++++++++++ .../AppKit/NSAppleEventDescriptor+RSCore.m | 23 +++++++++++++++++++ Frameworks/RSCore/RSCore/RSCore.h | 2 ++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.h create mode 100644 Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.m diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index 8e6e5309b..bd5e5bab1 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -103,6 +103,10 @@ 84B99C9A1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; }; 84B99C9B1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; }; 84BB45431D6909C700B48537 /* NSMutableDictionary-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */; }; + 84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */; }; + 84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */; }; + 84C632A5200D356E007BEEAA /* SendToBlogEditorApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */; }; 84C687301FBAA30800345C9E /* LogWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84C6872F1FBAA30800345C9E /* LogWindow.xib */; }; 84C687321FBAA3DF00345C9E /* LogWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C687311FBAA3DF00345C9E /* LogWindowController.swift */; }; 84C687351FBC025600345C9E /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C687341FBC025600345C9E /* Log.swift */; }; @@ -220,6 +224,10 @@ 84B99C931FAE64D400ECDEDB /* DisplayNameProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisplayNameProvider.swift; path = RSCore/DisplayNameProvider.swift; sourceTree = ""; }; 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OPMLRepresentable.swift; path = RSCore/OPMLRepresentable.swift; sourceTree = ""; }; 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableDictionary-Extensions.swift"; sourceTree = ""; }; + 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSAppleEventDescriptor+RSCore.h"; path = "AppKit/NSAppleEventDescriptor+RSCore.h"; sourceTree = ""; }; + 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSAppleEventDescriptor+RSCore.m"; path = "AppKit/NSAppleEventDescriptor+RSCore.m"; sourceTree = ""; }; + 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SendToBlogEditorApp.h; path = AppKit/SendToBlogEditorApp.h; sourceTree = ""; }; + 84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SendToBlogEditorApp.m; path = AppKit/SendToBlogEditorApp.m; sourceTree = ""; }; 84C6872F1FBAA30800345C9E /* LogWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = LogWindow.xib; path = AppKit/LogWindow.xib; sourceTree = ""; }; 84C687311FBAA3DF00345C9E /* LogWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LogWindowController.swift; path = AppKit/LogWindowController.swift; sourceTree = ""; }; 84C687341FBC025600345C9E /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; @@ -436,6 +444,10 @@ isa = PBXGroup; children = ( 84CFF5511AC3CF4700CEA6C8 /* NSColor+RSCore.h */, + 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */, + 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */, + 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */, + 84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */, 84CFF5521AC3CF4700CEA6C8 /* NSColor+RSCore.m */, 8415CB881BF84D24007B1E98 /* NSEvent+RSCore.h */, 8415CB891BF84D24007B1E98 /* NSEvent+RSCore.m */, @@ -547,6 +559,7 @@ 84CFF4FA1AC3C69700CEA6C8 /* RSCore.h in Headers */, 844F91D51D90D86100820C48 /* RSTransparentContainerView.h in Headers */, 84CFF53F1AC3CD0100CEA6C8 /* NSMutableSet+RSCore.h in Headers */, + 84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */, 84CFF5121AC3C6D800CEA6C8 /* RSBlocks.h in Headers */, 84CFF56D1AC3D20A00CEA6C8 /* NSImage+RSCore.h in Headers */, 84CFF5471AC3CD8000CEA6C8 /* NSTimer+RSCore.h in Headers */, @@ -558,6 +571,7 @@ 84CFF52F1AC3CB1900CEA6C8 /* NSDate+RSCore.h in Headers */, 8414CBAB1C95F8F700333C12 /* RSGeometry.h in Headers */, 845DE0F31B80477100D1571B /* NSSet+RSCore.h in Headers */, + 84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */, 844C915B1B65753E0051FC1B /* RSPlist.h in Headers */, 8453F7DE1BDF337800B1C8ED /* RSMacroProcessor.h in Headers */, 8415CB8A1BF84D24007B1E98 /* NSEvent+RSCore.h in Headers */, @@ -751,6 +765,7 @@ 84CFF5171AC3C73000CEA6C8 /* RSConstants.m in Sources */, 8432B1881DACA2060057D6DF /* NSWindow-Extensions.swift in Sources */, 8402047E1FBCE77900D94C1A /* BatchUpdate.swift in Sources */, + 84C632A5200D356E007BEEAA /* SendToBlogEditorApp.m in Sources */, 84CFF5541AC3CF4700CEA6C8 /* NSColor+RSCore.m in Sources */, 84536F671BB856D4001E1639 /* NSFileManager+RSCore.m in Sources */, 8415CB8B1BF84D24007B1E98 /* NSEvent+RSCore.m in Sources */, @@ -772,6 +787,7 @@ 84C687351FBC025600345C9E /* Log.swift in Sources */, 84CFF5301AC3CB1900CEA6C8 /* NSDate+RSCore.m in Sources */, 84CFF5281AC3C9A200CEA6C8 /* NSArray+RSCore.m in Sources */, + 84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */, 84E72E171FBD647500B873C1 /* InspectorView.swift in Sources */, 84CFF5591AC3CF9100CEA6C8 /* NSView+RSCore.m in Sources */, 84CFF56A1AC3D1B000CEA6C8 /* RSScaling.m in Sources */, @@ -935,7 +951,6 @@ GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_SHADOW = YES; GCC_WARN_SIGN_COMPARE = YES; @@ -1007,7 +1022,6 @@ GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_SHADOW = YES; GCC_WARN_SIGN_COMPARE = YES; diff --git a/Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.h b/Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.h new file mode 100644 index 000000000..6bd02b28e --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.h @@ -0,0 +1,19 @@ +// +// NSAppleEventDescriptor+RSCore.h +// RSCore +// +// Created by Brent Simmons on 1/15/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +@import Cocoa; + +NS_ASSUME_NONNULL_BEGIN + +@interface NSAppleEventDescriptor (RSCore) + ++ (NSAppleEventDescriptor * _Nullable)descriptorWithRunningApplication:(NSRunningApplication *)runningApplication; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.m b/Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.m new file mode 100644 index 000000000..fb6a8710d --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/NSAppleEventDescriptor+RSCore.m @@ -0,0 +1,23 @@ +// +// NSAppleEventDescriptor+RSCore.m +// RSCore +// +// Created by Brent Simmons on 1/15/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +#import "NSAppleEventDescriptor+RSCore.h" + +@implementation NSAppleEventDescriptor (RSCore) + ++ (NSAppleEventDescriptor * _Nullable)descriptorWithRunningApplication:(NSRunningApplication *)runningApplication { + + pid_t processIdentifier = runningApplication.processIdentifier; + if (processIdentifier == -1) { + return nil; + } + + return [NSAppleEventDescriptor descriptorWithProcessIdentifier:processIdentifier]; +} + +@end diff --git a/Frameworks/RSCore/RSCore/RSCore.h b/Frameworks/RSCore/RSCore/RSCore.h index 797ae1ac2..4e7b9eca3 100755 --- a/Frameworks/RSCore/RSCore/RSCore.h +++ b/Frameworks/RSCore/RSCore/RSCore.h @@ -52,6 +52,8 @@ #import +#import + #endif From c63303f05a651b04718469b7abcdd7c7e5ca1de6 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 12:10:57 -0800 Subject: [PATCH 17/27] Add UserApp.targetDescriptor. --- Frameworks/RSCore/RSCore/AppKit/UserApp.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift index 00a71c55e..164192ad8 100644 --- a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift +++ b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift @@ -118,6 +118,18 @@ public final class UserApp { updateStatus() return runningApplication?.activate(options: [.activateIgnoringOtherApps]) ?? false } + + public func targetDescriptor() -> NSAppleEventDescriptor? { + + // Requires that the app has previously been launched. + + updateStatus() + guard let runningApplication = runningApplication, !runningApplication.isTerminated else { + return nil + } + + return NSAppleEventDescriptor(runningApplication: runningApplication) + } } From 77bc330d8c9d8d6ea241afc678a293596bcd11fc Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 12:11:28 -0800 Subject: [PATCH 18/27] Create SendToBlogEditorApp, which implements the sending side of the external blog editor interface: http://ranchero.com/netnewswire/developers/externalinterface --- .../RSCore/AppKit/SendToBlogEditorApp.h | 28 +++++ .../RSCore/AppKit/SendToBlogEditorApp.m | 114 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.h create mode 100644 Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m diff --git a/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.h b/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.h new file mode 100644 index 000000000..535ad2a21 --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.h @@ -0,0 +1,28 @@ +// +// SendToBlogEditorApp.h +// RSCore +// +// Created by Brent Simmons on 1/15/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +@import Cocoa; + +// This is for sending articles to MarsEdit and other apps that implement the send-to-blog-editor Apple events API: +// http://ranchero.com/netnewswire/developers/externalinterface +// +// The first parameter is a target descriptor. The easiest way to get this is probably UserApp.targetDescriptor or +[NSAppleEventDescriptor descriptorWithRunningApplication:]. +// This does not care of launching the app in the first place. See UserApp.swift. + +NS_ASSUME_NONNULL_BEGIN + +@interface SendToBlogEditorApp : NSObject + +- (instancetype)initWithTargetDesciptor:(NSAppleEventDescriptor *)targetDescriptor title:(NSString * _Nullable)title body:(NSString * _Nullable)body summary:(NSString * _Nullable)summary link:(NSString * _Nullable)link permalink:(NSString * _Nullable)permalink subject:(NSString * _Nullable)subject creator:(NSString * _Nullable)creator commentsURL:(NSString * _Nullable)commentsURL guid:(NSString * _Nullable)guid sourceName:(NSString * _Nullable)sourceName sourceHomeURL:(NSString * _Nullable)sourceHomeURL sourceFeedURL:(NSString * _Nullable)sourceFeedURL; + +- (OSStatus)send; // Actually send the Apple event. + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m b/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m new file mode 100644 index 000000000..b09ae0c57 --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m @@ -0,0 +1,114 @@ +// +// SendToBlogEditorApp.m +// RSCore +// +// Created by Brent Simmons on 1/15/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +#import "SendToBlogEditorApp.h" + +@interface SendToBlogEditorApp() + +@property (nonatomic, readonly) NSAppleEventDescriptor *targetDescriptor; + +@property (nonatomic, nullable, readonly) NSString *title; +@property (nonatomic, nullable, readonly) NSString *body; +@property (nonatomic, nullable, readonly) NSString *summary; +@property (nonatomic, nullable, readonly) NSString *link; +@property (nonatomic, nullable, readonly) NSString *permalink; +@property (nonatomic, nullable, readonly) NSString *subject; +@property (nonatomic, nullable, readonly) NSString *creator; +@property (nonatomic, nullable, readonly) NSString *commentsURL; +@property (nonatomic, nullable, readonly) NSString *guid; +@property (nonatomic, nullable, readonly) NSString *sourceName; +@property (nonatomic, nullable, readonly) NSString *sourceHomeURL; +@property (nonatomic, nullable, readonly) NSString *sourceFeedURL; + +@end + +@implementation SendToBlogEditorApp + +- (instancetype)initWithTargetDesciptor:(NSAppleEventDescriptor *)targetDescriptor title:(NSString * _Nullable)title body:(NSString * _Nullable)body summary:(NSString * _Nullable)summary link:(NSString * _Nullable)link permalink:(NSString * _Nullable)permalink subject:(NSString * _Nullable)subject creator:(NSString * _Nullable)creator commentsURL:(NSString * _Nullable)commentsURL guid:(NSString * _Nullable)guid sourceName:(NSString * _Nullable)sourceName sourceHomeURL:(NSString * _Nullable)sourceHomeURL sourceFeedURL:(NSString * _Nullable)sourceFeedURL { + + self = [super init]; + if (!self) { + return nil; + } + + _targetDescriptor = targetDescriptor; + _title = title; + _body = body; + _summary = summary; + _link = link; + _permalink = permalink; + _subject = subject; + _creator = creator; + _commentsURL = commentsURL; + _guid = guid; + _sourceName = sourceName; + _sourceHomeURL = sourceHomeURL; + _sourceFeedURL = sourceFeedURL; + + return self; +} + +const AEKeyword EditDataItemAppleEventClass = 'EBlg'; +const AEKeyword EditDataItemAppleEventID = 'oitm'; +const AEKeyword DataItemTitle = 'titl'; +const AEKeyword DataItemDescription = 'desc'; +const AEKeyword DataItemSummary = 'summ'; +const AEKeyword DataItemLink = 'link'; +const AEKeyword DataItemPermalink = 'plnk'; +const AEKeyword DataItemSubject = 'subj'; +const AEKeyword DataItemCreator = 'crtr'; +const AEKeyword DataItemCommentsURL = 'curl'; +const AEKeyword DataItemGUID = 'guid'; +const AEKeyword DataItemSourceName = 'snam'; +const AEKeyword DataItemSourceHomeURL = 'hurl'; +const AEKeyword DataItemSourceFeedURL = 'furl'; + +- (OSStatus)send { + + NSAppleEventDescriptor *appleEvent = [NSAppleEventDescriptor appleEventWithEventClass:EditDataItemAppleEventClass eventID:EditDataItemAppleEventID targetDescriptor:self.targetDescriptor returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID]; + + [appleEvent setParamDescriptor:[self paramDescriptor] forKeyword:keyDirectObject]; + + return AESendMessage((const AppleEvent *)[appleEvent aeDesc], NULL, kAENoReply | kAECanSwitchLayer | kAEAlwaysInteract, kAEDefaultTimeout); +} + +#pragma mark - Private + +- (NSAppleEventDescriptor *)paramDescriptor { + + NSAppleEventDescriptor *descriptor = [NSAppleEventDescriptor recordDescriptor]; + + [self addToDescriptor:descriptor key:@"title" keyword:DataItemTitle]; + [self addToDescriptor:descriptor key:@"body" keyword:DataItemDescription]; + [self addToDescriptor:descriptor key:@"summary" keyword:DataItemSummary]; + [self addToDescriptor:descriptor key:@"link" keyword:DataItemLink]; + [self addToDescriptor:descriptor key:@"permalink" keyword:DataItemPermalink]; + [self addToDescriptor:descriptor key:@"subject" keyword:DataItemSubject]; + [self addToDescriptor:descriptor key:@"creator" keyword:DataItemCreator]; + [self addToDescriptor:descriptor key:@"commentsURL" keyword:DataItemCommentsURL]; + [self addToDescriptor:descriptor key:@"guid" keyword:DataItemGUID]; + [self addToDescriptor:descriptor key:@"sourceName" keyword:DataItemSourceName]; + [self addToDescriptor:descriptor key:@"sourceHomeURL" keyword:DataItemSourceHomeURL]; + [self addToDescriptor:descriptor key:@"sourceFeedURL" keyword:DataItemSourceFeedURL]; + + return descriptor; +} + +- (void)addToDescriptor:(NSAppleEventDescriptor *)descriptor key:(NSString *)key keyword:(AEKeyword)keyword { + + NSString *stringValue = (NSString *)[self valueForKey:key]; + if (!stringValue) { + return; + } + + NSAppleEventDescriptor *stringDescriptor = [NSAppleEventDescriptor descriptorWithString:stringValue]; + [descriptor setDescriptor:stringDescriptor forKeyword:keyword]; +} + + +@end From 82f11f659233b3a7b712a190afea76d5df566bf2 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 12:11:56 -0800 Subject: [PATCH 19/27] Add send-to-MarsEdit command to list of of send-to commands. --- Evergreen/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index 3f8fe5e95..50ee09538 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -34,7 +34,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, }() lazy var sendToCommands: [SendToCommand] = { - return [SendToMicroBlogCommand()] //, SendToMarsEditCommand()] + return [SendToMicroBlogCommand(), SendToMarsEditCommand()] }() var unreadCount = 0 { From f4aca068eab7bda217a5d1fc3c79123856e0d588 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 12:21:54 -0800 Subject: [PATCH 20/27] Make SendToBlogEditorApp public in RSCore. --- Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj | 2 +- Frameworks/RSCore/RSCore/RSCore.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index bd5e5bab1..4f8276b12 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -105,7 +105,7 @@ 84BB45431D6909C700B48537 /* NSMutableDictionary-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */; }; 84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */; }; - 84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */; }; + 84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84C632A5200D356E007BEEAA /* SendToBlogEditorApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */; }; 84C687301FBAA30800345C9E /* LogWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84C6872F1FBAA30800345C9E /* LogWindow.xib */; }; 84C687321FBAA3DF00345C9E /* LogWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C687311FBAA3DF00345C9E /* LogWindowController.swift */; }; diff --git a/Frameworks/RSCore/RSCore/RSCore.h b/Frameworks/RSCore/RSCore/RSCore.h index 4e7b9eca3..6c832cf8e 100755 --- a/Frameworks/RSCore/RSCore/RSCore.h +++ b/Frameworks/RSCore/RSCore/RSCore.h @@ -53,6 +53,7 @@ #import #import +#import #endif From cedbf3f3f57f6cb19b7bda13fe5562b7c4c92874 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 12:22:12 -0800 Subject: [PATCH 21/27] Use SendToBlogEditorApp when sending an article to MarsEdit. --- Commands/SendToMarsEditCommand.swift | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Commands/SendToMarsEditCommand.swift b/Commands/SendToMarsEditCommand.swift index 9fc081ddf..1b9ec0f6e 100644 --- a/Commands/SendToMarsEditCommand.swift +++ b/Commands/SendToMarsEditCommand.swift @@ -8,6 +8,7 @@ import Cocoa import RSCore +import Data final class SendToMarsEditCommand: SendToCommand { @@ -29,14 +30,37 @@ final class SendToMarsEditCommand: SendToCommand { func sendObject(_ object: Any?, selectedText: String?) { - if !canSendObject(object, selectedText: selectedText) { + guard canSendObject(object, selectedText: selectedText) else { return } + guard let article = (object as? ArticlePasteboardWriter)?.article else { + return + } + guard let app = appToUse(), app.launchIfNeeded(), app.bringToFront() else { + return + } + + send(article, to: app) } } private extension SendToMarsEditCommand { + func send(_ article: Article, to app: UserApp) { + + // App has already been launched. + + guard let targetDescriptor = app.targetDescriptor() else { + return + } + + let body = article.contentHTML ?? article.contentText ?? article.summary + let authorName = article.authors?.first?.name + + let sender = SendToBlogEditorApp(targetDesciptor: targetDescriptor, title: article.title, body: body, summary: article.summary, link: article.externalURL, permalink: article.url, subject: nil, creator: authorName, commentsURL: nil, guid: article.uniqueID, sourceName: article.feed?.nameForDisplay, sourceHomeURL: article.feed?.homePageURL, sourceFeedURL: article.feed?.url) + let _ = sender.send() + } + func appToUse() -> UserApp? { marsEditApps.forEach{ $0.updateStatus() } From d1e915394ee7d0c94d1269839579c9aee0b06f80 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 12:28:35 -0800 Subject: [PATCH 22/27] Use properties rather than strings and KVC. Duh. --- .../RSCore/AppKit/SendToBlogEditorApp.m | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m b/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m index b09ae0c57..016f667d4 100644 --- a/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m +++ b/Frameworks/RSCore/RSCore/AppKit/SendToBlogEditorApp.m @@ -83,30 +83,29 @@ const AEKeyword DataItemSourceFeedURL = 'furl'; NSAppleEventDescriptor *descriptor = [NSAppleEventDescriptor recordDescriptor]; - [self addToDescriptor:descriptor key:@"title" keyword:DataItemTitle]; - [self addToDescriptor:descriptor key:@"body" keyword:DataItemDescription]; - [self addToDescriptor:descriptor key:@"summary" keyword:DataItemSummary]; - [self addToDescriptor:descriptor key:@"link" keyword:DataItemLink]; - [self addToDescriptor:descriptor key:@"permalink" keyword:DataItemPermalink]; - [self addToDescriptor:descriptor key:@"subject" keyword:DataItemSubject]; - [self addToDescriptor:descriptor key:@"creator" keyword:DataItemCreator]; - [self addToDescriptor:descriptor key:@"commentsURL" keyword:DataItemCommentsURL]; - [self addToDescriptor:descriptor key:@"guid" keyword:DataItemGUID]; - [self addToDescriptor:descriptor key:@"sourceName" keyword:DataItemSourceName]; - [self addToDescriptor:descriptor key:@"sourceHomeURL" keyword:DataItemSourceHomeURL]; - [self addToDescriptor:descriptor key:@"sourceFeedURL" keyword:DataItemSourceFeedURL]; + [self addToDescriptor:descriptor value:self.title keyword:DataItemTitle]; + [self addToDescriptor:descriptor value:self.body keyword:DataItemDescription]; + [self addToDescriptor:descriptor value:self.summary keyword:DataItemSummary]; + [self addToDescriptor:descriptor value:self.link keyword:DataItemLink]; + [self addToDescriptor:descriptor value:self.permalink keyword:DataItemPermalink]; + [self addToDescriptor:descriptor value:self.subject keyword:DataItemSubject]; + [self addToDescriptor:descriptor value:self.creator keyword:DataItemCreator]; + [self addToDescriptor:descriptor value:self.commentsURL keyword:DataItemCommentsURL]; + [self addToDescriptor:descriptor value:self.guid keyword:DataItemGUID]; + [self addToDescriptor:descriptor value:self.sourceName keyword:DataItemSourceName]; + [self addToDescriptor:descriptor value:self.sourceHomeURL keyword:DataItemSourceHomeURL]; + [self addToDescriptor:descriptor value:self.sourceFeedURL keyword:DataItemSourceFeedURL]; return descriptor; } -- (void)addToDescriptor:(NSAppleEventDescriptor *)descriptor key:(NSString *)key keyword:(AEKeyword)keyword { +- (void)addToDescriptor:(NSAppleEventDescriptor *)descriptor value:(NSString *)value keyword:(AEKeyword)keyword { - NSString *stringValue = (NSString *)[self valueForKey:key]; - if (!stringValue) { + if (!value) { return; } - NSAppleEventDescriptor *stringDescriptor = [NSAppleEventDescriptor descriptorWithString:stringValue]; + NSAppleEventDescriptor *stringDescriptor = [NSAppleEventDescriptor descriptorWithString:value]; [descriptor setDescriptor:stringDescriptor forKeyword:keyword]; } From f048fa607366ccedf92b8c64a4f12894a8d4402a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 14:07:43 -0800 Subject: [PATCH 23/27] Bump version. --- Evergreen/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index 7d05afa13..2289d6e49 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0d32 + 1.0d33 CFBundleVersion 522 LSMinimumSystemVersion From 228dee0461fd9ea4e40bef4593f6d0b37721ff88 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 15 Jan 2018 17:31:04 -0800 Subject: [PATCH 24/27] Update appcast for 1.0d33. --- Appcasts/evergreen-beta.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index efef9c912..55c52998b 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,20 @@ Most recent Evergreen changes with links to updates. en + + Evergreen 1.0d33 + Send to MarsEdit +

If you have MarsEdit on your Mac, then you can send an article from Evergreen to MarsEdit via the sharing menu in the toolbar.

+

In MarsEdit you can edit the post and add your commentary before posting to your blog.

+ + ]]>
+ Mon, 15 Jan 2018 14:10:00 -0800 + + 10.13 +
+ Evergreen 1.0d32 Date: Wed, 17 Jan 2018 17:28:09 -0800 Subject: [PATCH 25/27] Make progress on reloading timeline when feed updates. --- .../Timeline/TimelineViewController.swift | 78 +++++++++++++++++++ Frameworks/Account/Account.swift | 5 +- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index f96072ab0..870b2a952 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -52,6 +52,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { private var didRegisterForNotifications = false private let timelineFontSizeKVOKey = "values.{AppDefaults.Key.timelineFontSize}" private var reloadAvailableCellsTimer: Timer? + private var fetchAndMergeArticlesTimer: Timer? private var articles = ArticleArray() { didSet { @@ -120,6 +121,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) NSUserDefaultsController.shared.addObserver(self, forKeyPath: timelineFontSizeKVOKey, options: NSKeyValueObservingOptions(rawValue: 0), context: nil) @@ -369,6 +371,18 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } } + @objc func accountDidDownloadArticles(_ note: Notification) { + + guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set else { + return + } + + let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds) + if shouldFetchAndMergeArticles { + queueFetchAndMergeArticles() + } + } + // MARK: - Reloading Data private func cellForRowView(_ rowView: NSView) -> NSView? { @@ -645,6 +659,44 @@ private extension TimelineViewController { } } + + func fetchAndMergeArticles() { + + let selectedArticleIDs = selectedArticles.articleIDs() + + + selectArticles(selectedArticleIDs) + } + + func selectArticles(_ articleIDs: [String]) { + + let indexesToSelect = indexesOf(articleIDs) + if indexesToSelect.isEmpty { + tableView.deselectAll(self) + return + } + tableView.selectRowIndexes(indexesToSelect, byExtendingSelection: false) + } + + func invalidateFetchAndMergeArticlesTimer() { + + if let timer = fetchAndMergeArticlesTimer { + if timer.isValid { + timer.invalidate() + } + fetchAndMergeArticlesTimer = nil + } + } + + func queueFetchAndMergeArticles() { + + invalidateFetchAndMergeArticlesTimer() + fetchAndMergeArticlesTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { (timer) in + self.fetchAndMergeArticles() + self.invalidateFetchAndMergeArticlesTimer() + } + } + func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool { if objects1 == nil && objects2 == nil { @@ -666,5 +718,31 @@ private extension TimelineViewController { } return true } + + func representedObjectsContainsAnyFeed(_ feeds: Set) -> Bool { + + // Return true if there’s a match or if a folder contains (recursively) one of feeds + + guard let representedObjects = representedObjects else { + return false + } + for representedObject in representedObjects { + if let feed = representedObject as? Feed { + 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 feeds { + if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) { + return true + } + } + } + } + return false + } } diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 47d63c9d8..badef1d21 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -41,7 +41,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles public static let statuses = "statuses" // StatusesDidChange public static let articles = "articles" // StatusesDidChange - public static let feeds = "feeds" // StatusesDidChange + public static let feeds = "feeds" // AccountDidDownloadArticles, StatusesDidChange } public let accountID: String @@ -167,10 +167,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, if let updatedArticles = updatedArticles, !updatedArticles.isEmpty { userInfo[UserInfoKey.updatedArticles] = updatedArticles } + userInfo[UserInfoKey.feeds] = Set([feed]) completion() - NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo.isEmpty ? nil : userInfo) + NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo) } } From 21f1863cd02896ab542184a67177ba8280214dfd Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 17 Jan 2018 21:51:24 -0800 Subject: [PATCH 26/27] Unbreak the build by commenting out some in-progress code. --- .../MainWindow/Timeline/TimelineViewController.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 870b2a952..236cffb74 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -670,12 +670,12 @@ private extension TimelineViewController { func selectArticles(_ articleIDs: [String]) { - let indexesToSelect = indexesOf(articleIDs) - if indexesToSelect.isEmpty { - tableView.deselectAll(self) - return - } - tableView.selectRowIndexes(indexesToSelect, byExtendingSelection: false) +// let indexesToSelect = indexesOf(articleIDs) +// if indexesToSelect.isEmpty { +// tableView.deselectAll(self) +// return +// } +// tableView.selectRowIndexes(indexesToSelect, byExtendingSelection: false) } func invalidateFetchAndMergeArticlesTimer() { From 9fea9c2d126444ab13a4bd1d1f1a9686561878be Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 17 Jan 2018 22:03:13 -0800 Subject: [PATCH 27/27] Create and use fetchUnsortedArticles(for:), which is common code that needed to be a separate function. --- .../Timeline/TimelineViewController.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 236cffb74..1234c6072 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -641,6 +641,15 @@ private extension TimelineViewController { return } + let fetchedArticles = fetchUnsortedArticles(for: representedObjects) + let sortedArticles = Array(fetchedArticles).sortedByDate() + if articles != sortedArticles { + articles = sortedArticles + } + } + + func fetchUnsortedArticles(for representedObjects: [Any]) -> Set
{ + var fetchedArticles = Set
() for object in representedObjects { @@ -653,13 +662,9 @@ private extension TimelineViewController { } } - let sortedArticles = Array(fetchedArticles).sortedByDate() - if articles != sortedArticles { - articles = sortedArticles - } + return fetchedArticles } - func fetchAndMergeArticles() { let selectedArticleIDs = selectedArticles.articleIDs()