diff --git a/IntentsExtension/Extensions/FileManager+Intents.swift b/IntentsExtension/Extensions/FileManager+Intents.swift new file mode 100644 index 000000000..2ed5b6c11 --- /dev/null +++ b/IntentsExtension/Extensions/FileManager+Intents.swift @@ -0,0 +1,18 @@ +// +// FileManager+Intents.swift +// IntentsExtension +// +// Created by Charlie Scheer on 5/31/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Foundation + +// This exists in a8cTracks but we aren't currently importing that into intents but we need this for FileManager to meet FileManagerProtocol +extension FileManager { + func directoryExistsAtURL(_ url: URL) -> Bool { + var isDir: ObjCBool = false + let exists = self.fileExists(atPath: url.path, isDirectory: &isDir) + return exists && isDir.boolValue + } +} diff --git a/IntentsExtension/Extensions/IntentNote+Helpers.swift b/IntentsExtension/Extensions/IntentNote+Helpers.swift new file mode 100644 index 000000000..84cf06f30 --- /dev/null +++ b/IntentsExtension/Extensions/IntentNote+Helpers.swift @@ -0,0 +1,23 @@ +// +// IntentNote.swift +// IntentsExtension +// +// Created by Charlie Scheer on 5/29/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Intents + +extension IntentNote { + static func allNotes(in coreDataWrapper: ExtensionCoreDataWrapper) throws -> [IntentNote] { + guard let notes = coreDataWrapper.resultsController?.notes() else { + throw IntentsError.couldNotFetchNotes + } + + return makeIntentNotes(from: notes) + } + + static func makeIntentNotes(from notes: [Note]) -> [IntentNote] { + notes.map({ IntentNote(identifier: $0.simperiumKey, display: $0.title) }) + } +} diff --git a/IntentsExtension/IntentHandler.swift b/IntentsExtension/IntentHandler.swift index c65c2d924..38a70a374 100644 --- a/IntentsExtension/IntentHandler.swift +++ b/IntentsExtension/IntentHandler.swift @@ -13,6 +13,8 @@ class IntentHandler: INExtension { switch intent { case is OpenNewNoteIntent: return OpenNewNoteIntentHandler() + case is OpenNoteIntent: + return OpenNoteIntentHandler() default: return self } diff --git a/IntentsExtension/IntentHandlers/OpenNoteIntentHandler.swift b/IntentsExtension/IntentHandlers/OpenNoteIntentHandler.swift new file mode 100644 index 000000000..c1bbd0197 --- /dev/null +++ b/IntentsExtension/IntentHandlers/OpenNoteIntentHandler.swift @@ -0,0 +1,34 @@ +// +// OpenNoteIntentHandler.swift +// IntentsExtension +// +// Created by Charlie Scheer on 5/29/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Intents + +class OpenNoteIntentHandler: NSObject, OpenNoteIntentHandling { + let coredataWrapper = ExtensionCoreDataWrapper() + + func provideNoteOptionsCollection(for intent: OpenNoteIntent, with completion: @escaping (INObjectCollection?, (any Error)?) -> Void) { + do { + let intentNotes = try IntentNote.allNotes(in: coredataWrapper) + completion(INObjectCollection(items: intentNotes), nil) + } catch { + completion(nil, IntentsError.couldNotFetchNotes) + } + } + + func handle(intent: OpenNoteIntent, completion: @escaping (OpenNoteIntentResponse) -> Void) { + guard let identifier = intent.note?.identifier else { + completion(OpenNoteIntentResponse(code: .failure, userActivity: nil)) + return + } + + let activity = NSUserActivity(activityType: ActivityType.openNoteShortcut.rawValue) + activity.userInfo = [IntentsConstants.noteIdentifierKey: identifier] + + completion(OpenNoteIntentResponse(code: .continueInApp, userActivity: activity)) + } +} diff --git a/IntentsExtension/Models/Note+Intents.swift b/IntentsExtension/Models/Note+Intents.swift new file mode 100644 index 000000000..0ac2c7397 --- /dev/null +++ b/IntentsExtension/Models/Note+Intents.swift @@ -0,0 +1,61 @@ +// This file contains the required class structure to be able to fetch and use core data files in widgets and intents +// We have collapsed the auto generated core data files into a single file as it is unlikely that the files will need to +// be regenerated. Contained in this file is the generated class files Note+CoreDataClass.swift and Note+CoreDataProperties.swift + +import Foundation +import CoreData + +@objc(Note) +public class Note: SPManagedObject { + +} + +extension Note { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Note") + } + + public override func awakeFromInsert() { + super.awakeFromInsert() + + if simperiumKey.isEmpty { + simperiumKey = UUID().uuidString.replacingOccurrences(of: "-", with: "") + } + } + + @NSManaged public var content: String? + @NSManaged public var creationDate: Date? + @NSManaged public override var isDeleted: Bool + @NSManaged public var lastPosition: NSNumber? + @NSManaged public var modificationDate: Date? + @NSManaged public var noteSynced: NSNumber? + @NSManaged public var owner: String? + @NSManaged public var pinned: NSNumber? + @NSManaged public var publishURL: String? + @NSManaged public var remoteId: String? + @NSManaged public var shareURL: String? + @NSManaged public var systemTags: String? + @NSManaged public var tags: String? +} + +extension Note { + var title: String { + let noteStructure = NoteContentHelper.structure(of: content) + return title(with: noteStructure.title) + } + + private func title(with range: Range?) -> String { + guard let range = range, let content = content else { + return Constants.defaultTitle + } + + let result = String(content[range]) + return result.droppingPrefix(Constants.titleMarkdownPrefix) + } +} + +private struct Constants { + static let defaultTitle = NSLocalizedString("New Note...", comment: "Default title for notes") + static let titleMarkdownPrefix = "# " +} diff --git a/IntentsExtension/Models/SPManagedObject+Intents.swift b/IntentsExtension/Models/SPManagedObject+Intents.swift new file mode 100644 index 000000000..73c0eb618 --- /dev/null +++ b/IntentsExtension/Models/SPManagedObject+Intents.swift @@ -0,0 +1,21 @@ +// This file contains the required class structure to be able to fetch and use core data files in widgets and intents +// We have collapsed the auto generated core data files into a single file as it is unlikely that the files will need to +// be regenerated. Contained in this file is the generated class files SPManagedObject+CoreDataClass.swift and SPManagedObject+CoreDataProperties.swift + +import Foundation +import CoreData + +@objc(SPManagedObject) +public class SPManagedObject: NSManagedObject { + +} + +extension SPManagedObject { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "SPManagedObject") + } + + @NSManaged public var ghostData: String? + @NSManaged public var simperiumKey: String +} diff --git a/IntentsExtension/Info.plist b/IntentsExtension/Support Files/Info.plist similarity index 86% rename from IntentsExtension/Info.plist rename to IntentsExtension/Support Files/Info.plist index 4a2d61e2e..2dbe338f8 100644 --- a/IntentsExtension/Info.plist +++ b/IntentsExtension/Support Files/Info.plist @@ -13,6 +13,7 @@ IntentsSupported OpenNewNoteIntent + OpenNoteIntent NSExtensionPointIdentifier @@ -20,7 +21,7 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).IntentHandler - TeamIDPrefix - $(TeamIdentifierPrefix) + TeamIDPrefix + $(TeamIdentifierPrefix) diff --git a/IntentsExtension/IntentsExtension.entitlements b/IntentsExtension/Support Files/IntentsExtension.entitlements similarity index 100% rename from IntentsExtension/IntentsExtension.entitlements rename to IntentsExtension/Support Files/IntentsExtension.entitlements diff --git a/IntentsExtension/IntentsExtensionDebug.entitlements b/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements similarity index 100% rename from IntentsExtension/IntentsExtensionDebug.entitlements rename to IntentsExtension/Support Files/IntentsExtensionDebug.entitlements diff --git a/IntentsExtension/Tools/ExtensionCoreDataWrapper.swift b/IntentsExtension/Tools/ExtensionCoreDataWrapper.swift new file mode 100644 index 000000000..c6428f3dd --- /dev/null +++ b/IntentsExtension/Tools/ExtensionCoreDataWrapper.swift @@ -0,0 +1,34 @@ +// +// ExtensionCoreDataWrapper.swift +// IntentsExtension +// +// Created by Charlie Scheer on 5/29/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Foundation +import CoreData + +class ExtensionCoreDataWrapper { + private lazy var coreDataManager: CoreDataManager? = { + do { + return try CoreDataManager(storageSettings: StorageSettings(), for: .intents) + } catch { + return nil + } + }() + + lazy var resultsController: ExtensionResultsController? = { + guard let coreDataManager else { + return nil + } + return ExtensionResultsController(context: coreDataManager.managedObjectContext) + }() + + func context() -> NSManagedObjectContext? { + guard let coreDataManager else { + return nil + } + return coreDataManager.managedObjectContext + } +} diff --git a/IntentsExtension/Tools/ExtensionResultsController.swift b/IntentsExtension/Tools/ExtensionResultsController.swift new file mode 100644 index 000000000..47a04baf1 --- /dev/null +++ b/IntentsExtension/Tools/ExtensionResultsController.swift @@ -0,0 +1,68 @@ +// +// ExtensionResultsController.swift +// IntentsExtension +// +// Created by Charlie Scheer on 5/29/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Foundation +import CoreData +import SimplenoteSearch +import SimplenoteFoundation + +class ExtensionResultsController { + + /// Data Controller + /// + let managedObjectContext: NSManagedObjectContext + + /// Initialization + /// + init(context: NSManagedObjectContext) { + self.managedObjectContext = context + } + + // MARK: - Notes + + /// Fetch notes with given tag and limit + /// If no tag is specified, will fetch notes that are not deleted. If there is no limit specified it will fetch all of the notes + /// + func notes(limit: Int = .zero) -> [Note]? { + let request: NSFetchRequest = fetchRequestForNotes(limit: limit) + return performFetch(from: request) + } + + /// Returns note given a simperium key + /// + func note(forSimperiumKey key: String) -> Note? { + return notes()?.first { note in + note.simperiumKey == key + } + } + + func noteExists(forSimperiumKey key: String) -> Bool { + note(forSimperiumKey: key) != nil + } + + private func fetchRequestForNotes(limit: Int = .zero) -> NSFetchRequest { + let fetchRequest = NSFetchRequest(entityName: Note.entityName) + fetchRequest.fetchLimit = limit + fetchRequest.sortDescriptors = [NSSortDescriptor.descriptorForNotes(sortMode: .alphabeticallyAscending)] + fetchRequest.predicate = NSPredicate.predicateForNotes(deleted: false) + + return fetchRequest + } + + // MARK: Fetching + + private func performFetch(from request: NSFetchRequest) -> [T]? { + do { + let objects = try managedObjectContext.fetch(request) + return objects + } catch { + NSLog("Couldn't fetch objects: %@", error.localizedDescription) + return nil + } + } +} diff --git a/IntentsExtension/Tools/IntentsConstants.swift b/IntentsExtension/Tools/IntentsConstants.swift new file mode 100644 index 000000000..664275a30 --- /dev/null +++ b/IntentsExtension/Tools/IntentsConstants.swift @@ -0,0 +1,13 @@ +// +// IntentsConstants.swift +// IntentsExtension +// +// Created by Charlie Scheer on 5/29/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Foundation + +struct IntentsConstants { + static let noteIdentifierKey = "OpenNoteIntentHandlerIdentifierKey" +} diff --git a/IntentsExtension/Tools/IntentsError.swift b/IntentsExtension/Tools/IntentsError.swift new file mode 100644 index 000000000..200090005 --- /dev/null +++ b/IntentsExtension/Tools/IntentsError.swift @@ -0,0 +1,27 @@ +// +// IntentsError.swift +// IntentsExtension +// +// Created by Charlie Scheer on 5/29/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Foundation + +enum IntentsError: Error { + case couldNotFetchNotes + + var title: String { + switch self { + case .couldNotFetchNotes: + return NSLocalizedString("Could not fetch Notes", comment: "Note fetch error title") + } + } + + var message: String { + switch self { + case .couldNotFetchNotes: + return NSLocalizedString("Attempt to fetch notes failed. Please try again later.", comment: "Data Fetch error message") + } + } +} diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 116315c2e..a346a63b1 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -270,6 +270,29 @@ 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 */; }; + BAAA71D52C07A10E00244C01 /* SPManagedObject+Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71D42C07A10E00244C01 /* SPManagedObject+Intents.swift */; }; + BAAA71D72C07A11500244C01 /* Note+Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71D62C07A11500244C01 /* Note+Intents.swift */; }; + BAAA71D82C07A12F00244C01 /* NoteContentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6672FB225C7F12F00090DE3 /* NoteContentHelper.swift */; }; + BAAA71D92C07A14600244C01 /* String+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500993B24213B370037A431 /* String+Simplenote.swift */; }; + BAAA71DA2C07A18200244C01 /* ContentSlice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6672FB725C7F1BB00090DE3 /* ContentSlice.swift */; }; + BAAA71DD2C07A1DF00244C01 /* ExtensionResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71DC2C07A1DF00244C01 /* ExtensionResultsController.swift */; }; + BAAA71DF2C07A25C00244C01 /* SimplenoteSearch in Frameworks */ = {isa = PBXBuildFile; productRef = BAAA71DE2C07A25C00244C01 /* SimplenoteSearch */; }; + BAAA71E12C07A2A600244C01 /* SimplenoteFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = BAAA71E02C07A2A600244C01 /* SimplenoteFoundation */; }; + BAAA71E22C07A2BA00244C01 /* NSSortDescriptor+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5469FCF2587FCD9007ED7BE /* NSSortDescriptor+Simplenote.swift */; }; + BAAA71E32C07A2C200244C01 /* SortMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5469FC92587DE3F007ED7BE /* SortMode.swift */; }; + BAAA71E42C07A2F900244C01 /* NSUserInterfaceItemIdentifier+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5070E15245938D600715BCC /* NSUserInterfaceItemIdentifier+Simplenote.swift */; }; + BAAA71E62C07A35F00244C01 /* ExtensionCoreDataWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71E52C07A35F00244C01 /* ExtensionCoreDataWrapper.swift */; }; + BAAA71E72C07A37000244C01 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB2617D2BFFDD6B009A98D7 /* CoreDataManager.swift */; }; + BAAA71E82C07A37700244C01 /* StorageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB2617F2BFFDD97009A98D7 /* StorageSettings.swift */; }; + BAAA71E92C07A39300244C01 /* FileManager+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC5DFB32C0691E6002AD7EF /* FileManager+Simplenote.swift */; }; + BAAA71EA2C07A58100244C01 /* Simplenote.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 2614F1E21405A0C60031AE94 /* Simplenote.xcdatamodeld */; }; + BAAA71ED2C07A69C00244C01 /* IntentNote+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71EC2C07A69C00244C01 /* IntentNote+Helpers.swift */; }; + BAAA71EF2C07A6CF00244C01 /* IntentsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */; }; + BAAA71F02C07A7DB00244C01 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB2617A2BFFD40B009A98D7 /* ActivityType.swift */; }; + BAAA71F22C07A7FD00244C01 /* IntentsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */; }; + BAAA71F32C07AB8E00244C01 /* IntentsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */; }; + BAAD56472C0A42000047E024 /* FileManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A658E2C090FEC00F605A6 /* FileManagerProtocol.swift */; }; + BAAD56492C0A42E40047E024 /* FileManager+Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD56482C0A42E40047E024 /* FileManager+Intents.swift */; }; BAB261632BFFD0AF009A98D7 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA8CF21B2BFD20770087F33D /* Intents.framework */; }; BAB261662BFFD0AF009A98D7 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB261652BFFD0AF009A98D7 /* IntentHandler.swift */; }; BAB2616B2BFFD0AF009A98D7 /* IntentsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BAB261622BFFD0AF009A98D7 /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -283,6 +306,7 @@ BAC5DFB42C0691E6002AD7EF /* FileManager+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC5DFB32C0691E6002AD7EF /* FileManager+Simplenote.swift */; }; BAC5DFB52C069568002AD7EF /* Bundle+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57CB87D244DED2300BA7969 /* Bundle+Simplenote.swift */; }; BAC5DFB72C069993002AD7EF /* SharedStorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC5DFB62C069993002AD7EF /* SharedStorageMigrator.swift */; }; + BAC5DFB92C079E5A002AD7EF /* OpenNoteIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC5DFB82C079E5A002AD7EF /* OpenNoteIntentHandler.swift */; }; BAD4ECC026E6FC0A00881CC4 /* markdown-light.css in Resources */ = {isa = PBXBuildFile; fileRef = BAF8D5DB26AE3BE800CA9383 /* markdown-light.css */; }; BAE66CAA26AF647500398FF3 /* Remote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA938CEB26ACFF4A00BE5A1D /* Remote.swift */; }; BAF1B0C32BC9B1C500B55F73 /* NoteWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF1B0C12BC9B1C500B55F73 /* NoteWindow.swift */; }; @@ -684,6 +708,14 @@ 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 = ""; }; BAA4854925D5E22000F3BDB9 /* SearchQuery+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchQuery+Simplenote.swift"; sourceTree = ""; }; + BAAA71D42C07A10E00244C01 /* SPManagedObject+Intents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SPManagedObject+Intents.swift"; sourceTree = ""; }; + BAAA71D62C07A11500244C01 /* Note+Intents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Note+Intents.swift"; sourceTree = ""; }; + BAAA71DC2C07A1DF00244C01 /* ExtensionResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionResultsController.swift; sourceTree = ""; }; + BAAA71E52C07A35F00244C01 /* ExtensionCoreDataWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionCoreDataWrapper.swift; sourceTree = ""; }; + BAAA71EC2C07A69C00244C01 /* IntentNote+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentNote+Helpers.swift"; sourceTree = ""; }; + BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsError.swift; sourceTree = ""; }; + BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsConstants.swift; sourceTree = ""; }; + BAAD56482C0A42E40047E024 /* FileManager+Intents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Intents.swift"; sourceTree = ""; }; BAB261622BFFD0AF009A98D7 /* IntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; BAB261652BFFD0AF009A98D7 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; BAB261672BFFD0AF009A98D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -695,6 +727,7 @@ BAB2617F2BFFDD97009A98D7 /* StorageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettings.swift; sourceTree = ""; }; BAC5DFB32C0691E6002AD7EF /* FileManager+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Simplenote.swift"; sourceTree = ""; }; BAC5DFB62C069993002AD7EF /* SharedStorageMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStorageMigrator.swift; sourceTree = ""; }; + BAC5DFB82C079E5A002AD7EF /* OpenNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenNoteIntentHandler.swift; sourceTree = ""; }; BAF1B0C12BC9B1C500B55F73 /* NoteWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteWindow.swift; sourceTree = ""; }; BAF1B0C72BCDDA9600B55F73 /* NoteWindowControllersManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteWindowControllersManager.swift; sourceTree = ""; }; BAF8D5DB26AE3BE800CA9383 /* markdown-light.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = "markdown-light.css"; sourceTree = ""; }; @@ -733,6 +766,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BAAA71DF2C07A25C00244C01 /* SimplenoteSearch in Frameworks */, + BAAA71E12C07A2A600244C01 /* SimplenoteFoundation in Frameworks */, BAB261632BFFD0AF009A98D7 /* Intents.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1533,14 +1568,44 @@ name = Protocols; sourceTree = ""; }; + BAAA71D32C07A0F000244C01 /* Models */ = { + isa = PBXGroup; + children = ( + BAAA71D62C07A11500244C01 /* Note+Intents.swift */, + BAAA71D42C07A10E00244C01 /* SPManagedObject+Intents.swift */, + ); + path = Models; + sourceTree = ""; + }; + BAAA71DB2C07A1D000244C01 /* Tools */ = { + isa = PBXGroup; + children = ( + BAAA71DC2C07A1DF00244C01 /* ExtensionResultsController.swift */, + BAAA71E52C07A35F00244C01 /* ExtensionCoreDataWrapper.swift */, + BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */, + BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */, + ); + path = Tools; + sourceTree = ""; + }; + BAAA71EB2C07A69300244C01 /* Extensions */ = { + isa = PBXGroup; + children = ( + BAAA71EC2C07A69C00244C01 /* IntentNote+Helpers.swift */, + BAAD56482C0A42E40047E024 /* FileManager+Intents.swift */, + ); + path = Extensions; + sourceTree = ""; + }; BAB261642BFFD0AF009A98D7 /* IntentsExtension */ = { isa = PBXGroup; children = ( - BA39C4552C0134180004B2A9 /* IntentsExtensionDebug.entitlements */, + BAAA71EB2C07A69300244C01 /* Extensions */, BAB261732BFFD2F6009A98D7 /* IntentHandlers */, + BAAA71D32C07A0F000244C01 /* Models */, + BAC5DFBB2C079F20002AD7EF /* Support Files */, + BAAA71DB2C07A1D000244C01 /* Tools */, BAB261652BFFD0AF009A98D7 /* IntentHandler.swift */, - BAB261672BFFD0AF009A98D7 /* Info.plist */, - BAB261682BFFD0AF009A98D7 /* IntentsExtension.entitlements */, ); path = IntentsExtension; sourceTree = ""; @@ -1549,10 +1614,21 @@ isa = PBXGroup; children = ( BAB261742BFFD319009A98D7 /* OpenNewNoteIntentHandler.swift */, + BAC5DFB82C079E5A002AD7EF /* OpenNoteIntentHandler.swift */, ); path = IntentHandlers; sourceTree = ""; }; + BAC5DFBB2C079F20002AD7EF /* Support Files */ = { + isa = PBXGroup; + children = ( + BA39C4552C0134180004B2A9 /* IntentsExtensionDebug.entitlements */, + BAB261682BFFD0AF009A98D7 /* IntentsExtension.entitlements */, + BAB261672BFFD0AF009A98D7 /* Info.plist */, + ); + path = "Support Files"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1618,6 +1694,10 @@ dependencies = ( ); name = IntentsExtension; + packageProductDependencies = ( + BAAA71DE2C07A25C00244C01 /* SimplenoteSearch */, + BAAA71E02C07A2A600244C01 /* SimplenoteFoundation */, + ); productName = IntentsExtension; productReference = BAB261622BFFD0AF009A98D7 /* IntentsExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -1978,6 +2058,7 @@ 375D294521E033D1007AB25A /* html.c in Sources */, B5E8E41224575C990098892B /* ToolbarState.swift in Sources */, B5070E17245938D600715BCC /* NSUserInterfaceItemIdentifier+Simplenote.swift in Sources */, + BAAA71F32C07AB8E00244C01 /* IntentsConstants.swift in Sources */, B5C6334A251E6E3200C8BF46 /* LinkTableCellView.swift in Sources */, BAA0A88B26BA39150006260E /* Date+Simplenote.swift in Sources */, B542FE4725D42E6D00A3582D /* SearchMapView.swift in Sources */, @@ -2109,10 +2190,31 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BAAA71EF2C07A6CF00244C01 /* IntentsError.swift in Sources */, + BAAA71F22C07A7FD00244C01 /* IntentsConstants.swift in Sources */, + BAAA71D92C07A14600244C01 /* String+Simplenote.swift in Sources */, + BAAA71E92C07A39300244C01 /* FileManager+Simplenote.swift in Sources */, + BAAA71E82C07A37700244C01 /* StorageSettings.swift in Sources */, + BAAA71E22C07A2BA00244C01 /* NSSortDescriptor+Simplenote.swift in Sources */, + BAAA71E62C07A35F00244C01 /* ExtensionCoreDataWrapper.swift in Sources */, BAB261722BFFD0F4009A98D7 /* ShortcutIntents.intentdefinition in Sources */, + BAAA71DA2C07A18200244C01 /* ContentSlice.swift in Sources */, + BAAD56492C0A42E40047E024 /* FileManager+Intents.swift in Sources */, + BAAA71F02C07A7DB00244C01 /* ActivityType.swift in Sources */, + BAAA71E32C07A2C200244C01 /* SortMode.swift in Sources */, + BAAA71E42C07A2F900244C01 /* NSUserInterfaceItemIdentifier+Simplenote.swift in Sources */, BAB261762BFFD319009A98D7 /* OpenNewNoteIntentHandler.swift in Sources */, + BAAA71D72C07A11500244C01 /* Note+Intents.swift in Sources */, + BAAA71D82C07A12F00244C01 /* NoteContentHelper.swift in Sources */, + BAAA71EA2C07A58100244C01 /* Simplenote.xcdatamodeld in Sources */, BAB261662BFFD0AF009A98D7 /* IntentHandler.swift in Sources */, + BAAD56472C0A42000047E024 /* FileManagerProtocol.swift in Sources */, BAC5DFB52C069568002AD7EF /* Bundle+Simplenote.swift in Sources */, + BAAA71DD2C07A1DF00244C01 /* ExtensionResultsController.swift in Sources */, + BAC5DFB92C079E5A002AD7EF /* OpenNoteIntentHandler.swift in Sources */, + BAAA71E72C07A37000244C01 /* CoreDataManager.swift in Sources */, + BAAA71D52C07A10E00244C01 /* SPManagedObject+Intents.swift in Sources */, + BAAA71ED2C07A69C00244C01 /* IntentNote+Helpers.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2548,7 +2650,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = IntentsExtension/IntentsExtensionDebug.entitlements; + CODE_SIGN_ENTITLEMENTS = "IntentsExtension/Support Files/IntentsExtensionDebug.entitlements"; CODE_SIGN_IDENTITY = "Mac Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; CODE_SIGN_STYLE = Manual; @@ -2562,7 +2664,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = IntentsExtension/Info.plist; + INFOPLIST_FILE = "IntentsExtension/Support Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = IntentsExtension; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Simperium. All rights reserved."; LD_RUNPATH_SEARCH_PATHS = ( @@ -2603,7 +2705,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = IntentsExtension/IntentsExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = "IntentsExtension/Support Files/IntentsExtension.entitlements"; CODE_SIGN_IDENTITY = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; CODE_SIGN_STYLE = Manual; @@ -2618,7 +2720,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = IntentsExtension/Info.plist; + INFOPLIST_FILE = "IntentsExtension/Support Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = IntentsExtension; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Simperium. All rights reserved."; LD_RUNPATH_SEARCH_PATHS = ( @@ -2750,6 +2852,16 @@ package = BA78AF6D2B5B2BAE00DCF896 /* XCRemoteSwiftPackageReference "Automattic-Tracks-iOS" */; productName = AutomatticTracks; }; + BAAA71DE2C07A25C00244C01 /* SimplenoteSearch */ = { + isa = XCSwiftPackageProductDependency; + package = B5609AE624EEC9860097777A /* XCRemoteSwiftPackageReference "SimplenoteSearch-Swift" */; + productName = SimplenoteSearch; + }; + BAAA71E02C07A2A600244C01 /* SimplenoteFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = B5609AEE24EF171D0097777A /* XCRemoteSwiftPackageReference "SimplenoteFoundation-Swift" */; + productName = SimplenoteFoundation; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index d2db68724..000000000 --- a/Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,69 +0,0 @@ -{ - "originHash" : "1f66ac352dc04da5870abacba5f42306bd560111ad295c7c60a13a7af5c25ec8", - "pins" : [ - { - "identity" : "automattic-tracks-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Automattic/Automattic-Tracks-iOS", - "state" : { - "revision" : "cc3200463d9b97d2c58d839d1534e77e5bfa66c3", - "version" : "3.4.1" - } - }, - { - "identity" : "sentry-cocoa", - "kind" : "remoteSourceControl", - "location" : "https://github.com/getsentry/sentry-cocoa", - "state" : { - "revision" : "7fc7ca43967e2980d8691a8e017c118a84133aac", - "version" : "8.26.0" - } - }, - { - "identity" : "simplenotefoundation-swift", - "kind" : "remoteSourceControl", - "location" : "git@github.com:Automattic/SimplenoteFoundation-Swift.git", - "state" : { - "revision" : "2731e4d28c42d394ddc62cd864d9f7ff96759228", - "version" : "1.3.0" - } - }, - { - "identity" : "simplenoteinterlinks-swift", - "kind" : "remoteSourceControl", - "location" : "git@github.com:Automattic/SimplenoteInterlinks-Swift.git", - "state" : { - "revision" : "be3827a5bf05c5349ed62126c3e1dc60a2a6cee6", - "version" : "1.1.0" - } - }, - { - "identity" : "simplenotesearch-swift", - "kind" : "remoteSourceControl", - "location" : "git@github.com:Automattic/SimplenoteSearch-Swift.git", - "state" : { - "revision" : "499d2809d169fcbeb9ff75568d9f1f937f290ffc", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-sodium", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jedisct1/swift-sodium", - "state" : { - "revision" : "4f9164a0a2c9a6a7ff53a2833d54a5c79c957342", - "version" : "0.9.1" - } - }, - { - "identity" : "uideviceidentifier", - "kind" : "remoteSourceControl", - "location" : "https://github.com/squarefrog/UIDeviceIdentifier", - "state" : { - "revision" : "4699794b08bb79a4d77785edaba6ea739e298e4b", - "version" : "2.3.0" - } - } - ], - "version" : 3 -} diff --git a/Simplenote/ActivityType.swift b/Simplenote/ActivityType.swift index b877ca3c7..d8f51f279 100644 --- a/Simplenote/ActivityType.swift +++ b/Simplenote/ActivityType.swift @@ -10,4 +10,5 @@ import Foundation enum ActivityType: String { case newNoteShortcut = "OpenNewNoteIntent" + case openNoteShortcut = "OpenNoteIntent" } diff --git a/Simplenote/Bundle+Simplenote.swift b/Simplenote/Bundle+Simplenote.swift index 60addd8a2..b1df5974e 100644 --- a/Simplenote/Bundle+Simplenote.swift +++ b/Simplenote/Bundle+Simplenote.swift @@ -56,7 +56,7 @@ private struct Constants { static let defaultBundleID: String = { #if DEBUG - "com.automattic.SimplenoteMac.Debug" + "com.automattic.SimplenoteMac.Development" #else "com.automattic.SimplenoteMac" #endif diff --git a/Simplenote/CoreDataManager.swift b/Simplenote/CoreDataManager.swift index 4093a0659..11fd81814 100644 --- a/Simplenote/CoreDataManager.swift +++ b/Simplenote/CoreDataManager.swift @@ -21,13 +21,18 @@ enum CoreDataManagerError: Error { } } +enum CoreDataUsageType { + case standard + case intents +} + @objcMembers class CoreDataManager: NSObject { private(set) var managedObjectModel: NSManagedObjectModel private(set) var managedObjectContext: NSManagedObjectContext private(set) var persistentStoreCoordinator: NSPersistentStoreCoordinator - init(storageSettings: StorageSettings) throws { + init(storageSettings: StorageSettings, for usageType: CoreDataUsageType = .standard) throws { guard let modelURL = storageSettings.modelURL, let mom = NSManagedObjectModel(contentsOf: modelURL) else { throw CoreDataManagerError.couldNotBuildModel @@ -40,6 +45,9 @@ class CoreDataManager: NSObject { self.managedObjectModel = mom self.managedObjectContext = context self.persistentStoreCoordinator = psc + + super.init() + setupCoreDataStackIfNeeded(usage: usageType) } static private func preparePSC(at storageURL: URL, model: NSManagedObjectModel) throws -> NSPersistentStoreCoordinator { @@ -53,4 +61,12 @@ class CoreDataManager: NSObject { return coordinator } + + private func setupCoreDataStackIfNeeded(usage: CoreDataUsageType) { + guard usage != .standard else { + return + } + + managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator + } } diff --git a/Simplenote/NSUserInterfaceItemIdentifier+Simplenote.swift b/Simplenote/NSUserInterfaceItemIdentifier+Simplenote.swift index f06b7143e..2fe3d6e07 100644 --- a/Simplenote/NSUserInterfaceItemIdentifier+Simplenote.swift +++ b/Simplenote/NSUserInterfaceItemIdentifier+Simplenote.swift @@ -1,4 +1,5 @@ import Foundation +import AppKit // MARK: - Simplenote Extensible Constants // diff --git a/Simplenote/ShortcutIntents.intentdefinition b/Simplenote/ShortcutIntents.intentdefinition index 6c3dff0ac..167958fd0 100644 --- a/Simplenote/ShortcutIntents.intentdefinition +++ b/Simplenote/ShortcutIntents.intentdefinition @@ -68,8 +68,188 @@ INIntentVerb Open + + INIntentCategory + information + INIntentConfigurable + + INIntentDescription + Open Note in Simplenote + INIntentDescriptionID + tisdpm + INIntentIneligibleForSuggestions + + INIntentInput + note + INIntentLastParameterTag + 2 + INIntentManagedParameterCombinations + + note + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Open ${note} in Simplenote + INIntentParameterCombinationTitleID + 7wqmje + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + OpenNote + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Note + INIntentParameterDisplayNameID + heyFyD + INIntentParameterDisplayPriority + 1 + INIntentParameterName + note + INIntentParameterObjectType + IntentNote + INIntentParameterObjectTypeNamespace + ihfyvu + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${note}’. + INIntentParameterPromptDialogFormatStringID + PFbjPa + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${note}’? + INIntentParameterPromptDialogFormatStringID + 071o1f + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterTag + 2 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Open Note + INIntentTitleID + guPtXm + INIntentType + Custom + INIntentVerb + Open + INTypes - + + + INTypeDisplayName + Note + INTypeDisplayNameID + SfOvKd + INTypeLastPropertyTag + 99 + INTypeName + IntentNote + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + + diff --git a/Simplenote/Simplenote-Info.plist b/Simplenote/Simplenote-Info.plist index 29a639bc4..f2d3782fb 100644 --- a/Simplenote/Simplenote-Info.plist +++ b/Simplenote/Simplenote-Info.plist @@ -57,8 +57,9 @@ NSUserActivityTypes OpenNewNoteIntent + OpenNoteIntent - TeamIDPrefix - $(TeamIdentifierPrefix) + TeamIDPrefix + $(TeamIdentifierPrefix) diff --git a/Simplenote/SimplenoteAppDelegate+Swift.swift b/Simplenote/SimplenoteAppDelegate+Swift.swift index 2b8cd6e4b..fe93c3dfe 100644 --- a/Simplenote/SimplenoteAppDelegate+Swift.swift +++ b/Simplenote/SimplenoteAppDelegate+Swift.swift @@ -294,10 +294,21 @@ extension SimplenoteAppDelegate { switch type { case .newNoteShortcut: noteEditorViewController.createNote(from: nil) + case .openNoteShortcut: + presentNote(for: userActivity) } return true } + + func presentNote(for userActivity: NSUserActivity) { + guard let uniqueIdentifier = userActivity.userInfo?[IntentsConstants.noteIdentifierKey] as? String else { + return + } + + noteListViewController.displayAndSelectNote(with: uniqueIdentifier) + _ = window.makeFirstResponder(noteEditorViewController.noteEditor) + } } // MARK: - URL Handlers diff --git a/Simplenote/SortMode.swift b/Simplenote/SortMode.swift index fe79dc85e..0b8d9bb84 100644 --- a/Simplenote/SortMode.swift +++ b/Simplenote/SortMode.swift @@ -1,4 +1,5 @@ import Foundation +import AppKit // MARK: - Sort Modes // diff --git a/Simplenote/String+Simplenote.swift b/Simplenote/String+Simplenote.swift index afdc3734f..17662425c 100644 --- a/Simplenote/String+Simplenote.swift +++ b/Simplenote/String+Simplenote.swift @@ -1,4 +1,5 @@ import Foundation +import AppKit // MARK: - String Constants //