diff --git a/IntentsExtension/Extensions/NSURLSessionConfiguration+Extensions.swift b/IntentsExtension/Extensions/NSURLSessionConfiguration+Extensions.swift new file mode 100644 index 000000000..971289349 --- /dev/null +++ b/IntentsExtension/Extensions/NSURLSessionConfiguration+Extensions.swift @@ -0,0 +1,21 @@ +// +// NSURLSessionConfiguration+Extensions.swift +// IntentsExtension +// +// Created by Charlie Scheer on 6/3/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Foundation + +extension URLSessionConfiguration { + /// Returns a new Background Session Configuration, with a random identifier. + /// + class func backgroundSessionConfigurationWithRandomizedIdentifier() -> URLSessionConfiguration { + let identifier = IntentsConstants.extensionGroupName + "." + UUID().uuidString + let configuration = URLSessionConfiguration.background(withIdentifier: identifier) + configuration.sharedContainerIdentifier = IntentsConstants.extensionGroupName + + return configuration + } +} diff --git a/IntentsExtension/IntentHandler.swift b/IntentsExtension/IntentHandler.swift index 917434bf7..429439d13 100644 --- a/IntentsExtension/IntentHandler.swift +++ b/IntentsExtension/IntentHandler.swift @@ -21,6 +21,8 @@ class IntentHandler: INExtension { return FindNoteWithTagIntentHandler() case is CopyNoteContentIntent: return CopyNoteContentIntentHandler() + case is AppendNoteIntent: + return AppendNoteIntentHandler() default: return self } diff --git a/IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift b/IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift new file mode 100644 index 000000000..8fba54e70 --- /dev/null +++ b/IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift @@ -0,0 +1,40 @@ +import Intents + +class AppendNoteIntentHandler: NSObject, AppendNoteIntentHandling { + let coreDataWrapper = ExtensionCoreDataWrapper() + + func resolveContent(for intent: AppendNoteIntent) async -> INStringResolutionResult { + guard let content = intent.content else { + return INStringResolutionResult.needsValue() + } + return INStringResolutionResult.success(with: content) + } + + func resolveNote(for intent: AppendNoteIntent) async -> IntentNoteResolutionResult { + IntentNoteResolutionResult.resolve(intent.note, in: coreDataWrapper) + } + + func provideNoteOptionsCollection(for intent: AppendNoteIntent) async throws -> INObjectCollection { + let intentNotes = try IntentNote.allNotes(in: coreDataWrapper) + return INObjectCollection(items: intentNotes) + } + + func handle(intent: AppendNoteIntent) async -> AppendNoteIntentResponse { + guard let identifier = intent.note?.identifier, + let content = intent.content, + let note = coreDataWrapper.resultsController?.note(forSimperiumKey: identifier), + let token = KeychainManager.extensionToken else { + return AppendNoteIntentResponse(code: .failure, userActivity: nil) + } + + guard let existingContent = try? await Downloader(simperiumToken: token).getNoteContent(for: identifier) else { + return AppendNoteIntentResponse(code: .failure, userActivity: nil) + } + + note.content = existingContent + "\n\(content)" + let uploader = Uploader(simperiumToken: token) + uploader.send(note) + + return AppendNoteIntentResponse(code: .success, userActivity: nil) + } +} diff --git a/IntentsExtension/Models/Note+Intents.swift b/IntentsExtension/Models/Note+Intents.swift index 0ac2c7397..f839876b9 100644 --- a/IntentsExtension/Models/Note+Intents.swift +++ b/IntentsExtension/Models/Note+Intents.swift @@ -53,6 +53,55 @@ extension Note { let result = String(content[range]) return result.droppingPrefix(Constants.titleMarkdownPrefix) } + + private func objectFromJSONString(_ json: String) -> Any? { + guard let data = json.data(using: .utf8) else { + return nil + } + + return try? JSONSerialization.jsonObject(with: data) + } + + var tagsArray: [String] { + guard let tagsString = tags, + let array = tagsString.objectFromJSONString() as? [String] else { + return [] + } + + return array + } + + var systemTagsArray: [String] { + guard let systemTagsString = systemTags, + let array = systemTagsString.objectFromJSONString() as? [String] else { + return [] + } + + return array + } + + func toDictionary() -> [String: Any] { + + return [ + "tags": tagsArray, + "deleted": 0, + "shareURL": shareURL ?? String(), + "publishURL": publishURL ?? String(), + "content": content ?? "", + "systemTags": systemTagsArray, + "creationDate": (creationDate ?? .now).timeIntervalSince1970, + "modificationDate": (modificationDate ?? .now).timeIntervalSince1970 + ] + } + + func toJsonData() -> Data? { + do { + return try JSONSerialization.data(withJSONObject: toDictionary(), options: .prettyPrinted) + } catch { + print("Error converting Note to JSON: \(error)") + return nil + } + } } private struct Constants { diff --git a/IntentsExtension/Support Files/Info.plist b/IntentsExtension/Support Files/Info.plist index afa802481..604bcbfb1 100644 --- a/IntentsExtension/Support Files/Info.plist +++ b/IntentsExtension/Support Files/Info.plist @@ -13,6 +13,7 @@ IntentsSupported CopyNoteContentIntent + AppendNoteIntent FindNoteIntent FindNoteWithTagIntent OpenNewNoteIntent diff --git a/IntentsExtension/Support Files/IntentsExtension.entitlements b/IntentsExtension/Support Files/IntentsExtension.entitlements index cb817ac5e..29675a5ce 100644 --- a/IntentsExtension/Support Files/IntentsExtension.entitlements +++ b/IntentsExtension/Support Files/IntentsExtension.entitlements @@ -8,6 +8,10 @@ $(TeamIdentifierPrefix)com.automattic.SimplenoteMac + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + keychain-access-groups $(AppIdentifierPrefix)com.automattic.SimplenoteMac diff --git a/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements b/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements index 0b8d9cd0d..8ad56bac5 100644 --- a/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements +++ b/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements @@ -8,6 +8,10 @@ $(TeamIdentifierPrefix)com.automattic.SimplenoteMac.Development + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + keychain-access-groups $(AppIdentifierPrefix)com.automattic.SimplenoteMac.Development diff --git a/IntentsExtension/Tools/Downloader.swift b/IntentsExtension/Tools/Downloader.swift new file mode 100644 index 000000000..6f80656ad --- /dev/null +++ b/IntentsExtension/Tools/Downloader.swift @@ -0,0 +1,43 @@ +import Foundation + +class Downloader: NSObject { + + /// Simperium's Token + /// + private let token: String + + /// Designated Initializer + /// + init(simperiumToken: String) { + token = simperiumToken + } + + func getNoteContent(for simperiumKey: String) async throws -> String? { + let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, SPCredentials.simperiumAppID, Settings.bucketName, simperiumKey) + let targetURL = URL(string: endpoint.lowercased())! + + // Request + var request = URLRequest(url: targetURL) + request.httpMethod = Settings.httpMethodGet + request.setValue(token, forHTTPHeaderField: Settings.authHeader) + + let session = Foundation.URLSession(configuration: .default, delegate: nil, delegateQueue: .main) + + let downloadedData = try await session.data(for: request) + + return try extractNoteContent(from: downloadedData.0) + } + + private func extractNoteContent(from data: Data) throws -> String? { + let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] + return jsonObject?["content"] as? String + } +} + +// MARK: - Settings +// +private struct Settings { + static let authHeader = "X-Simperium-Token" + static let bucketName = "note" + static let httpMethodGet = "GET" +} diff --git a/IntentsExtension/Tools/IntentsConstants.swift b/IntentsExtension/Tools/IntentsConstants.swift index 664275a30..28aff5fd6 100644 --- a/IntentsExtension/Tools/IntentsConstants.swift +++ b/IntentsExtension/Tools/IntentsConstants.swift @@ -10,4 +10,6 @@ import Foundation struct IntentsConstants { static let noteIdentifierKey = "OpenNoteIntentHandlerIdentifierKey" + static let extensionGroupName = Bundle.main.sharedGroupDomain + static let simperiumBaseURL = "https://api.simperium.com/1" } diff --git a/IntentsExtension/Tools/Uploader.swift b/IntentsExtension/Tools/Uploader.swift new file mode 100644 index 000000000..2526cc013 --- /dev/null +++ b/IntentsExtension/Tools/Uploader.swift @@ -0,0 +1,70 @@ +import Foundation + +/// The purpose of this class is to encapsulate NSURLSession's interaction code, required to upload +/// a note to Simperium's REST endpoint. +/// +class Uploader: NSObject { + + /// Simperium's Token + /// + private let token: String + + /// Designated Initializer + /// + init(simperiumToken: String) { + token = simperiumToken + } + + // MARK: - Public Methods + func send(_ note: Note) { + // Build the targetURL + let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, SPCredentials.simperiumAppID, Settings.bucketName, note.simperiumKey) + let targetURL = URL(string: endpoint.lowercased())! + + // Request + var request = URLRequest(url: targetURL) + request.httpMethod = Settings.httpMethodPost + request.httpBody = note.toJsonData() + request.setValue(token, forHTTPHeaderField: Settings.authHeader) + + // Task! + let sc = URLSessionConfiguration.backgroundSessionConfigurationWithRandomizedIdentifier() + + let session = Foundation.URLSession(configuration: sc, delegate: self, delegateQueue: .main) + let task = session.downloadTask(with: request) + task.resume() + } +} + +// MARK: - URLSessionDelegate +// +extension Uploader: URLSessionDelegate { + + @objc + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + print("<> Uploader.didBecomeInvalidWithError: \(String(describing: error))") + } + + @objc + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + print("<> Uploader.URLSessionDidFinishEventsForBackgroundURLSession") + } +} + +// MARK: - URLSessionTaskDelegate +// +extension Uploader: URLSessionTaskDelegate { + + @objc + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + print("<> Uploader.didCompleteWithError: \(String(describing: error))") + } +} + +// MARK: - Settings +// +private struct Settings { + static let authHeader = "X-Simperium-Token" + static let bucketName = "note" + static let httpMethodPost = "POST" +} diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 97083a525..fc2ffea6e 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -256,7 +256,8 @@ BA2C65CF26FE996A00FA84E1 /* NSButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2C65CA26FE996100FA84E1 /* NSButton+Extensions.swift */; }; BA4C6D16264CA8C000B723A7 /* SignupRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D15264CA8C000B723A7 /* SignupRemoteTests.swift */; }; BA4C6D18264CAAF800B723A7 /* URLRequest+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */; }; - BA4F223D2C1117E500144EDA /* SPCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E196C0230F5F5300F5658A /* SPCredentials.swift */; }; + BA4F223E2C1255A500144EDA /* SPCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E196C0230F5F5300F5658A /* SPCredentials.swift */; }; + BA54F2462C0E63C700DBCE9D /* AppendNoteIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */; }; BA553F0927065E20007737E9 /* FontSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA553F0727065E20007737E9 /* FontSettings.swift */; }; BA5A658A2C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */; }; BA5A658C2C090F4800F605A6 /* SharedStorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */; }; @@ -270,6 +271,9 @@ BA71EC252BC88FFC00F42CB1 /* NSManagedObjectContext+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */; }; BA78AF712B5B2BC300DCF896 /* AutomatticTracks in Frameworks */ = {isa = PBXBuildFile; productRef = BA78AF702B5B2BC300DCF896 /* AutomatticTracks */; }; BA938CEE26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */; }; + BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB642C0E46CC00B34EA7 /* Uploader.swift */; }; + BA94CB682C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */; }; + BA94CB6F2C0E4AFA00B34EA7 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */; }; BAA0A88B26BA39150006260E /* Date+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */; }; BAA0A88F26BA39200006260E /* AccountDeletionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88126B9F8970006260E /* AccountDeletionController.swift */; }; BAA0A89326BA39260006260E /* RemoteError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A87926B9F0B50006260E /* RemoteError.swift */; }; @@ -706,6 +710,7 @@ BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Simplenote.swift"; sourceTree = ""; }; BA52005A2BC878F1003F1B75 /* CSSearchable+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CSSearchable+Helpers.swift"; sourceTree = ""; }; BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Simplenote.swift"; sourceTree = ""; }; + BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppendNoteIntentHandler.swift; sourceTree = ""; }; BA553F0727065E20007737E9 /* FontSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSettings.swift; sourceTree = ""; }; BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyNoteContentIntentHandler.swift; sourceTree = ""; }; BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStorageMigratorTests.swift; sourceTree = ""; }; @@ -718,6 +723,10 @@ BA8CF21B2BFD20770087F33D /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; BA938CEB26ACFF4A00BE5A1D /* Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Remote.swift; sourceTree = ""; }; BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountVerificationController+TestHelpers.swift"; sourceTree = ""; }; + BA94CB642C0E46CC00B34EA7 /* Uploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uploader.swift; sourceTree = ""; }; + BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSURLSessionConfiguration+Extensions.swift"; sourceTree = ""; }; + BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; BAA0A87926B9F0B50006260E /* RemoteError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteError.swift; sourceTree = ""; }; BAA0A88126B9F8970006260E /* AccountDeletionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDeletionController.swift; sourceTree = ""; }; BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = ""; }; @@ -834,6 +843,7 @@ 26F72A8B14032D2A00A7935E /* Frameworks */ = { isa = PBXGroup; children = ( + BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */, 26F72A8F14032D2A00A7935E /* AppKit.framework */, 26F72A8C14032D2A00A7935E /* Cocoa.framework */, 26F72A9014032D2A00A7935E /* CoreData.framework */, @@ -1608,6 +1618,8 @@ BAAA71E52C07A35F00244C01 /* ExtensionCoreDataWrapper.swift */, BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */, BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */, + BA94CB642C0E46CC00B34EA7 /* Uploader.swift */, + BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */, ); path = Tools; sourceTree = ""; @@ -1617,6 +1629,7 @@ children = ( BAAA71EC2C07A69C00244C01 /* IntentNote+Helpers.swift */, BAAD56482C0A42E40047E024 /* FileManager+Intents.swift */, + BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */, BACA68702C0A7027005409C1 /* IntentTag+Helpers.swift */, BACA68752C0A72F3005409C1 /* NSString+Intents.swift */, ); @@ -1642,6 +1655,7 @@ BAB261742BFFD319009A98D7 /* OpenNewNoteIntentHandler.swift */, BAC5DFB82C079E5A002AD7EF /* OpenNoteIntentHandler.swift */, BA2BF33F2C07C75500A7C894 /* FindNoteIntentHandler.swift */, + BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */, BACA686E2C0A6E2F005409C1 /* FindNoteWithTagIntentHandler.swift */, BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */, ); @@ -2228,8 +2242,11 @@ BAAA71F22C07A7FD00244C01 /* IntentsConstants.swift in Sources */, BAAA71D92C07A14600244C01 /* String+Simplenote.swift in Sources */, BACA687C2C0A764C005409C1 /* KeychainPasswordItem.swift in Sources */, + BA4F223E2C1255A500144EDA /* SPCredentials.swift in Sources */, + BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */, BAAA71E92C07A39300244C01 /* FileManager+Simplenote.swift in Sources */, BAAA71E82C07A37700244C01 /* StorageSettings.swift in Sources */, + BA94CB6F2C0E4AFA00B34EA7 /* Downloader.swift in Sources */, BAAA71E22C07A2BA00244C01 /* NSSortDescriptor+Simplenote.swift in Sources */, BAAA71E62C07A35F00244C01 /* ExtensionCoreDataWrapper.swift in Sources */, BAB261722BFFD0F4009A98D7 /* ShortcutIntents.intentdefinition in Sources */, @@ -2246,6 +2263,7 @@ BAAA71D72C07A11500244C01 /* Note+Intents.swift in Sources */, BAAA71D82C07A12F00244C01 /* NoteContentHelper.swift in Sources */, BAAA71EA2C07A58100244C01 /* Simplenote.xcdatamodeld in Sources */, + BA54F2462C0E63C700DBCE9D /* AppendNoteIntentHandler.swift in Sources */, BAB261662BFFD0AF009A98D7 /* IntentHandler.swift in Sources */, BAAD56472C0A42000047E024 /* FileManagerProtocol.swift in Sources */, BAC5DFB52C069568002AD7EF /* Bundle+Simplenote.swift in Sources */, @@ -2256,6 +2274,7 @@ BAAA71E72C07A37000244C01 /* CoreDataManager.swift in Sources */, BAAA71D52C07A10E00244C01 /* SPManagedObject+Intents.swift in Sources */, BAAA71ED2C07A69C00244C01 /* IntentNote+Helpers.swift in Sources */, + BA94CB682C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Simplenote.xcodeproj/xcshareddata/xcschemes/IntentsExtension.xcscheme b/Simplenote.xcodeproj/xcshareddata/xcschemes/IntentsExtension.xcscheme new file mode 100644 index 000000000..084e1eab5 --- /dev/null +++ b/Simplenote.xcodeproj/xcshareddata/xcschemes/IntentsExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Simplenote/ShortcutIntents.intentdefinition b/Simplenote/ShortcutIntents.intentdefinition index 2d3e62f20..070c48add 100644 --- a/Simplenote/ShortcutIntents.intentdefinition +++ b/Simplenote/ShortcutIntents.intentdefinition @@ -600,6 +600,168 @@ INIntentVerb Do + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Append content to an existing note in Simplenote + INIntentDescriptionID + QW5IlD + INIntentIneligibleForSuggestions + + INIntentInput + note + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + note,content + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Append ${content} to ${note} + INIntentParameterCombinationTitleID + yhhw7r + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + AppendNote + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Note + INIntentParameterDisplayNameID + 67s2SR + INIntentParameterDisplayPriority + 1 + INIntentParameterName + note + INIntentParameterObjectType + IntentNote + INIntentParameterObjectTypeNamespace + ihfyvu + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${note}’. + INIntentParameterPromptDialogFormatStringID + cWzR46 + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${note}’? + INIntentParameterPromptDialogFormatStringID + lDws53 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterTag + 2 + INIntentParameterType + Object + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Content + INIntentParameterDisplayNameID + whQq9I + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValueID + 3ABNcl + INIntentParameterMetadataMultiline + + + INIntentParameterName + content + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + What would you like to append to ${note} + INIntentParameterPromptDialogFormatStringID + N2YveT + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Append To Note + INIntentTitleID + rjuYKo + INIntentType + Custom + INIntentVerb + Do + INTypes diff --git a/Simplenote/Simplenote-Info.plist b/Simplenote/Simplenote-Info.plist index 1c0c971bc..6575c5894 100644 --- a/Simplenote/Simplenote-Info.plist +++ b/Simplenote/Simplenote-Info.plist @@ -57,6 +57,7 @@ NSUserActivityTypes CopyNoteContentIntent + AppendNoteIntent FindNoteIntent FindNoteWithTagIntent OpenNewNoteIntent diff --git a/Simplenote/String+Simplenote.swift b/Simplenote/String+Simplenote.swift index 17662425c..27da8cf34 100644 --- a/Simplenote/String+Simplenote.swift +++ b/Simplenote/String+Simplenote.swift @@ -90,6 +90,14 @@ extension String { func replacingNewlinesWithSpaces() -> String { split(whereSeparator: { $0.isNewline }).joined(separator: " ") } + + func objectFromJSONString() -> Any? { + guard let data = self.data(using: .utf8) else { + return nil + } + + return try? JSONSerialization.jsonObject(with: data) + } } // MARK: - Searching for the first / last characters