Skip to content

Commit

Permalink
Merge pull request #1168 from Automattic/charlie/1145/find-note-with-tag
Browse files Browse the repository at this point in the history
Shortcuts: Find note with tag
  • Loading branch information
charliescheer authored Jun 4, 2024
2 parents 33fa550 + ef9192e commit de2b4e4
Show file tree
Hide file tree
Showing 14 changed files with 446 additions and 50 deletions.
18 changes: 10 additions & 8 deletions IntentsExtension/Extensions/IntentNote+Helpers.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
//
// IntentNote.swift
// IntentsExtension
//
// Created by Charlie Scheer on 5/29/24.
// Copyright © 2024 Simperium. All rights reserved.
//

import Intents

extension IntentNoteResolutionResult {
Expand All @@ -32,6 +24,16 @@ extension IntentNoteResolutionResult {
return resolve(intentNotes)
}

static func resolveIntentNote(forTag tag: IntentTag, in coreDataWrapper: ExtensionCoreDataWrapper) -> IntentNoteResolutionResult {
guard let notesForTag = coreDataWrapper.resultsController?.notes(filteredBy: .tag(tag.displayString)) else {
return IntentNoteResolutionResult.unsupported()
}

let intentNotes = IntentNote.makeIntentNotes(from: notesForTag)

return resolve(intentNotes)
}

private static func resolve(_ intentNotes: [IntentNote]) -> IntentNoteResolutionResult {
guard intentNotes.isEmpty == false else {
return IntentNoteResolutionResult.unsupported()
Expand Down
19 changes: 19 additions & 0 deletions IntentsExtension/Extensions/IntentTag+Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// IntentTag+Helpers.swift
// IntentsExtension
//
// Created by Charlie Scheer on 5/31/24.
// Copyright © 2024 Simperium. All rights reserved.
//

import Intents

extension IntentTag {
static func allTags(in coreDataWrapper: ExtensionCoreDataWrapper) throws -> [IntentTag] {
guard let tags = coreDataWrapper.resultsController?.tags() else {
throw IntentsError.couldNotFetchTags
}

return tags.map({ IntentTag(identifier: $0.simperiumKey, display: $0.name ?? String()) })
}
}
20 changes: 20 additions & 0 deletions IntentsExtension/Extensions/NSString+Intents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// NSString+Intents.swift
// IntentsExtension
//
// Created by Charlie Scheer on 5/31/24.
// Copyright © 2024 Simperium. All rights reserved.
//

import Foundation

extension NSString {
/// Encodes the receiver as a `Tag Hash`
///
@objc
var byEncodingAsTagHash: String {
precomposedStringWithCanonicalMapping
.lowercased()
.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? self as String
}
}
2 changes: 2 additions & 0 deletions IntentsExtension/IntentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class IntentHandler: INExtension {
return OpenNoteIntentHandler()
case is FindNoteIntent:
return FindNoteIntentHandler()
case is FindNoteWithTagIntent:
return FindNoteWithTagIntentHandler()
case is CopyNoteContentIntent:
return CopyNoteContentIntentHandler()
default:
Expand Down
39 changes: 39 additions & 0 deletions IntentsExtension/IntentHandlers/FindNoteWithTagIntentHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Intents

class FindNoteWithTagIntentHandler: NSObject, FindNoteWithTagIntentHandling {
let coreDataWrapper = ExtensionCoreDataWrapper()

func resolveNote(for intent: FindNoteWithTagIntent, with completion: @escaping (IntentNoteResolutionResult) -> Void) {
if let selectedNote = intent.note {
completion(IntentNoteResolutionResult.success(with: selectedNote))
return
}

guard let selectedTag = intent.tag else {
completion(IntentNoteResolutionResult.needsValue())
return
}

completion(IntentNoteResolutionResult.resolveIntentNote(forTag: selectedTag, in: coreDataWrapper))
}

func provideTagOptionsCollection(for intent: FindNoteWithTagIntent, with completion: @escaping (INObjectCollection<IntentTag>?, (any Error)?) -> Void) {
do {
let tags = try IntentTag.allTags(in: coreDataWrapper)
completion(INObjectCollection(items: tags), nil)
} catch {
completion(nil, error)
}
}

func handle(intent: FindNoteWithTagIntent, completion: @escaping (FindNoteWithTagIntentResponse) -> Void) {
guard let note = intent.note else {
completion(FindNoteWithTagIntentResponse(code: .failure, userActivity: nil))
return
}

let response = FindNoteWithTagIntentResponse(code: .success, userActivity: nil)
response.note = note
completion(response)
}
}
22 changes: 22 additions & 0 deletions IntentsExtension/Models/Tag+Intents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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 Tag+CoreDataClass.swift and Tag+CoreDataProperties.swift

import Foundation
import CoreData

@objc(Tag)
public class Tag: SPManagedObject {

}

extension Tag {

@nonobjc public class func fetchRequest() -> NSFetchRequest<Tag> {
return NSFetchRequest<Tag>(entityName: "Tag")
}

@NSManaged public var index: NSNumber?
@NSManaged public var name: String?
@NSManaged public var share: String?
}
1 change: 1 addition & 0 deletions IntentsExtension/Support Files/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<array>
<string>CopyNoteContentIntent</string>
<string>FindNoteIntent</string>
<string>FindNoteWithTagIntent</string>
<string>OpenNewNoteIntent</string>
<string>OpenNoteIntent</string>
</array>
Expand Down
8 changes: 0 additions & 8 deletions IntentsExtension/Tools/ExtensionCoreDataWrapper.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
//
// ExtensionCoreDataWrapper.swift
// IntentsExtension
//
// Created by Charlie Scheer on 5/29/24.
// Copyright © 2024 Simperium. All rights reserved.
//

import Foundation
import CoreData

Expand Down
91 changes: 79 additions & 12 deletions IntentsExtension/Tools/ExtensionResultsController.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
//
// ExtensionResultsController.swift
// IntentsExtension
//
// Created by Charlie Scheer on 5/29/24.
// Copyright © 2024 Simperium. All rights reserved.
//

import Foundation
import CoreData
import SimplenoteSearch
Expand All @@ -24,7 +16,7 @@ class ExtensionResultsController {
}

// 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
///
Expand All @@ -45,17 +37,77 @@ class ExtensionResultsController {
note(forSimperiumKey: key) != nil
}

private func fetchRequestForNotes(limit: Int = .zero) -> NSFetchRequest<Note> {
/// 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(filteredBy filter: TagsFilter = .allNotes, limit: Int = .zero) -> [Note]? {
if case let .tag(tag) = filter {
if !tagExists(tagName: tag) {
return nil
}
}

let request: NSFetchRequest<Note> = fetchRequestForNotes(filteredBy: filter, limit: limit)
return performFetch(from: request)
}

private func fetchRequestForNotes(filteredBy filter: TagsFilter = .allNotes, limit: Int = .zero) -> NSFetchRequest<Note> {
let fetchRequest = NSFetchRequest<Note>(entityName: Note.entityName)
fetchRequest.fetchLimit = limit
fetchRequest.sortDescriptors = [NSSortDescriptor.descriptorForNotes(sortMode: .alphabeticallyAscending)]
fetchRequest.predicate = NSPredicate.predicateForNotes(deleted: false)
fetchRequest.predicate = predicateForNotes(filteredBy: filter)

return fetchRequest
}

// MARK: Fetching
/// Creates a predicate for notes given a tag name. If not specified the predicate is for all notes that are not deleted
///
private func predicateForNotes(filteredBy tagFilter: TagsFilter = .allNotes) -> NSPredicate {
switch tagFilter {
case .allNotes:
return NSPredicate.predicateForNotes(deleted: false)
case .tag(let tag):
return NSCompoundPredicate(type: .and, subpredicates: [
NSPredicate.predicateForNotes(deleted: false),
NSPredicate.predicateForNotes(tag: tag)
])
}
}

// MARK: - Tags
//
func tags() -> [Tag]? {
performFetch(from: fetchRequestForTags())
}

private func fetchRequestForTags() -> NSFetchRequest<Tag> {
let fetchRequest = NSFetchRequest<Tag>(entityName: Tag.entityName)
fetchRequest.sortDescriptors = [NSSortDescriptor.descriptorForTags()]

return fetchRequest
}

private func tagExists(tagName: String) -> Bool {
return tagForName(tagName: tagName) != nil
}

private func tagForName(tagName: String) -> Tag? {
guard let tags = tags() else {
return nil
}

let targetTagHash = tagName.byEncodingAsTagHash
for tag in tags {
if tag.name?.byEncodingAsTagHash == targetTagHash {
return tag
}
}

return nil
}

// MARK: Fetching
//
private func performFetch<T: NSManagedObject>(from request: NSFetchRequest<T>) -> [T]? {
do {
let objects = try managedObjectContext.fetch(request)
Expand All @@ -66,3 +118,18 @@ class ExtensionResultsController {
}
}
}

enum TagsFilter {
case allNotes
case tag(String)
}

extension TagsFilter {
init(from tag: String?) {
guard let tag = tag else {
self = .allNotes
return
}
self = .tag(tag)
}
}
5 changes: 5 additions & 0 deletions IntentsExtension/Tools/IntentsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ import Foundation

enum IntentsError: Error {
case couldNotFetchNotes
case couldNotFetchTags

var title: String {
switch self {
case .couldNotFetchNotes:
return NSLocalizedString("Could not fetch Notes", comment: "Note fetch error title")
case .couldNotFetchTags:
return NSLocalizedString("Could not fetch Tags", comment: "Tag 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")
case .couldNotFetchTags:
return NSLocalizedString("Attempt to fetch tags failed. Please try again later.", comment: "Data Fetch error message")
}
}
}
Loading

0 comments on commit de2b4e4

Please sign in to comment.