Skip to content

Commit

Permalink
Merge pull request #1170 from Automattic/charlie/1145/append-to-note-mk2
Browse files Browse the repository at this point in the history
Shortcut: Append to note
  • Loading branch information
charliescheer authored Jun 7, 2024
2 parents 7ffd391 + 17ece7a commit 3c36ed1
Show file tree
Hide file tree
Showing 15 changed files with 524 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 2 additions & 0 deletions IntentsExtension/IntentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class IntentHandler: INExtension {
return FindNoteWithTagIntentHandler()
case is CopyNoteContentIntent:
return CopyNoteContentIntentHandler()
case is AppendNoteIntent:
return AppendNoteIntentHandler()
default:
return self
}
Expand Down
40 changes: 40 additions & 0 deletions IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift
Original file line number Diff line number Diff line change
@@ -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<IntentNote> {
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)
}
}
49 changes: 49 additions & 0 deletions IntentsExtension/Models/Note+Intents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions IntentsExtension/Support Files/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<key>IntentsSupported</key>
<array>
<string>CopyNoteContentIntent</string>
<string>AppendNoteIntent</string>
<string>FindNoteIntent</string>
<string>FindNoteWithTagIntent</string>
<string>OpenNewNoteIntent</string>
Expand Down
4 changes: 4 additions & 0 deletions IntentsExtension/Support Files/IntentsExtension.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<array>
<string>$(TeamIdentifierPrefix)com.automattic.SimplenoteMac</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.automattic.SimplenoteMac</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<array>
<string>$(TeamIdentifierPrefix)com.automattic.SimplenoteMac.Development</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.automattic.SimplenoteMac.Development</string>
Expand Down
43 changes: 43 additions & 0 deletions IntentsExtension/Tools/Downloader.swift
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 2 additions & 0 deletions IntentsExtension/Tools/IntentsConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
70 changes: 70 additions & 0 deletions IntentsExtension/Tools/Uploader.swift
Original file line number Diff line number Diff line change
@@ -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"
}
21 changes: 20 additions & 1 deletion Simplenote.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -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 */; };
Expand Down Expand Up @@ -706,6 +710,7 @@
BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Simplenote.swift"; sourceTree = "<group>"; };
BA52005A2BC878F1003F1B75 /* CSSearchable+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CSSearchable+Helpers.swift"; sourceTree = "<group>"; };
BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Simplenote.swift"; sourceTree = "<group>"; };
BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppendNoteIntentHandler.swift; sourceTree = "<group>"; };
BA553F0727065E20007737E9 /* FontSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSettings.swift; sourceTree = "<group>"; };
BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyNoteContentIntentHandler.swift; sourceTree = "<group>"; };
BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStorageMigratorTests.swift; sourceTree = "<group>"; };
Expand All @@ -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 = "<group>"; };
BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountVerificationController+TestHelpers.swift"; sourceTree = "<group>"; };
BA94CB642C0E46CC00B34EA7 /* Uploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uploader.swift; sourceTree = "<group>"; };
BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSURLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
BAA0A87926B9F0B50006260E /* RemoteError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteError.swift; sourceTree = "<group>"; };
BAA0A88126B9F8970006260E /* AccountDeletionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDeletionController.swift; sourceTree = "<group>"; };
BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -834,6 +843,7 @@
26F72A8B14032D2A00A7935E /* Frameworks */ = {
isa = PBXGroup;
children = (
BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */,
26F72A8F14032D2A00A7935E /* AppKit.framework */,
26F72A8C14032D2A00A7935E /* Cocoa.framework */,
26F72A9014032D2A00A7935E /* CoreData.framework */,
Expand Down Expand Up @@ -1608,6 +1618,8 @@
BAAA71E52C07A35F00244C01 /* ExtensionCoreDataWrapper.swift */,
BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */,
BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */,
BA94CB642C0E46CC00B34EA7 /* Uploader.swift */,
BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */,
);
path = Tools;
sourceTree = "<group>";
Expand All @@ -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 */,
);
Expand All @@ -1642,6 +1655,7 @@
BAB261742BFFD319009A98D7 /* OpenNewNoteIntentHandler.swift */,
BAC5DFB82C079E5A002AD7EF /* OpenNoteIntentHandler.swift */,
BA2BF33F2C07C75500A7C894 /* FindNoteIntentHandler.swift */,
BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */,
BACA686E2C0A6E2F005409C1 /* FindNoteWithTagIntentHandler.swift */,
BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */,
);
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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;
};
Expand Down
Loading

0 comments on commit 3c36ed1

Please sign in to comment.