From 395b3d7ac7aac63772ff2db589a8454d6fee3e22 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 28 Nov 2022 21:32:42 +0800 Subject: [PATCH] Allow setting labels on highlights --- .../Views/Highlights/HighlightsListCard.swift | 21 ++++++ .../Views/Highlights/HighlightsListView.swift | 12 ++++ .../Highlights/HighlightsListViewModel.swift | 21 +++++- .../App/Views/Labels/ApplyLabelsView.swift | 9 ++- .../App/Views/Labels/LabelsViewModel.swift | 27 +++++++ .../Views/WebReader/WebReaderContainer.swift | 4 ++ .../UpdateHighlightLabelsPublisher.swift | 72 +++++++++++++++++++ .../InternalLinkedItemLabel.swift | 4 +- .../Views/Article/OmnivoreWebView.swift | 10 ++- .../OmnivoreKit/Sources/Views/TextChip.swift | 16 ++++- 10 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateHighlightLabelsPublisher.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListCard.swift b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListCard.swift index beefe44d5..4a76cee41 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListCard.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListCard.swift @@ -11,6 +11,7 @@ struct HighlightsListCard: View { @Binding var hasHighlightMutations: Bool let onSaveAnnotation: (String) -> Void let onDeleteHighlight: () -> Void + let onSetLabels: (String) -> Void var contextMenuView: some View { Group { @@ -30,6 +31,12 @@ struct HighlightsListCard: View { }, label: { Label("Copy", systemImage: "doc.on.doc") } ) + Button( + action: { + onSetLabels(highlightParams.highlightID) + }, + label: { Label("Labels", systemImage: "tag") } + ) Button( action: onDeleteHighlight, label: { Label("Delete", systemImage: "trash") } @@ -109,6 +116,20 @@ struct HighlightsListCard: View { } else { noteSection } + + if highlightParams.labels.count > 0 { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(highlightParams.labels, id: \.self) { + TextChip(feedItemLabel: $0) + } + Spacer() + } + }.introspectScrollView { scrollView in + scrollView.bounces = false + } + .padding(.top, 0) + } } .padding(.bottom, 8) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift index 50de7f421..5f1461b91 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListView.swift @@ -11,6 +11,7 @@ struct HighlightsListView: View { let itemObjectID: NSManagedObjectID @Binding var hasHighlightMutations: Bool + @State var setLabelsHighlight: Highlight? var emptyView: some View { Text(""" @@ -61,10 +62,21 @@ struct HighlightsListView: View { highlightID: highlightParams.highlightID, dataService: dataService ) + }, + onSetLabels: { highlightID in + setLabelsHighlight = Highlight.lookup(byID: highlightID, inContext: dataService.viewContext) } ) } } + }.sheet(item: $setLabelsHighlight) { highlight in + ApplyLabelsView(mode: .highlight(highlight), onSave: { selectedLabels in + hasHighlightMutations = true + + viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID, + labels: selectedLabels, + dataService: dataService) + }) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListViewModel.swift index 6eca3b4e9..f46bf7552 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Highlights/HighlightsListViewModel.swift @@ -10,6 +10,7 @@ struct HighlightListItemParams: Identifiable { let title: String let annotation: String let quote: String + let labels: [LinkedItemLabel] } @MainActor final class HighlightsListViewModel: ObservableObject { @@ -29,7 +30,8 @@ struct HighlightListItemParams: Identifiable { highlightID: highlightID, title: highlightItems[index].title, annotation: annotation, - quote: highlightItems[index].quote + quote: highlightItems[index].quote, + labels: highlightItems[index].labels ) } } @@ -39,6 +41,20 @@ struct HighlightListItemParams: Identifiable { highlightItems.removeAll { $0.highlightID == highlightID } } + func setLabelsForHighlight(highlightID: String, labels: [LinkedItemLabel], dataService: DataService) { + dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: labels.map(\.unwrappedID)) + + if let index = highlightItems.firstIndex(where: { $0.highlightID == highlightID }) { + highlightItems[index] = HighlightListItemParams( + highlightID: highlightID, + title: highlightItems[index].title, + annotation: highlightItems[index].annotation, + quote: highlightItems[index].quote, + labels: labels + ) + } + } + private func loadHighlights(item: LinkedItem) { let unsortedHighlights = item.highlights.asArray(of: Highlight.self) @@ -51,7 +67,8 @@ struct HighlightListItemParams: Identifiable { highlightID: $0.unwrappedID, title: "Highlight", annotation: $0.annotation ?? "", - quote: $0.quote ?? "" + quote: $0.quote ?? "", + labels: $0.labels.asArray(of: LinkedItemLabel.self) ) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 8fe26ea81..84793a0fe 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -6,11 +6,12 @@ import Views struct ApplyLabelsView: View { enum Mode { case item(LinkedItem) + case highlight(Highlight) case list([LinkedItemLabel]) var navTitle: String { switch self { - case .item: + case .item, .highlight: return "Assign Labels" case .list: return "Apply Label Filters" @@ -19,7 +20,7 @@ struct ApplyLabelsView: View { var confirmButtonText: String { switch self { - case .item: + case .item, .highlight: return "Save" case .list: return "Apply" @@ -109,6 +110,8 @@ struct ApplyLabelsView: View { switch mode { case let .item(feedItem): viewModel.saveItemLabelChanges(itemID: feedItem.unwrappedID, dataService: dataService) + case .highlight: + onSave?(viewModel.selectedLabels) case .list: onSave?(viewModel.selectedLabels) } @@ -148,6 +151,8 @@ struct ApplyLabelsView: View { switch mode { case let .item(feedItem): await viewModel.loadLabels(dataService: dataService, item: feedItem) + case let .highlight(highlight): + await viewModel.loadLabels(dataService: dataService, highlight: highlight) case let .list(labels): await viewModel.loadLabels(dataService: dataService, initiallySelectedLabels: labels) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 3ee32d76c..f66634fbd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -36,6 +36,29 @@ import Views isLoading = false } + func loadLabels( + dataService: DataService, + highlight: Highlight + ) async { + isLoading = true + + if let labelIDs = try? await dataService.labels() { + dataService.viewContext.performAndWait { + self.labels = labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel } + } + let selLabels = highlight.labels ?? [] + for label in labels { + if selLabels.contains(label) { + selectedLabels.append(label) + } else { + unselectedLabels.append(label) + } + } + } + + isLoading = false + } + func loadLabelsFromStore(dataService: DataService) async { let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() @@ -95,6 +118,10 @@ import Views dataService.updateItemLabels(itemID: itemID, labelIDs: selectedLabels.map(\.unwrappedID)) } + func saveHighlightLabelChanges(highlightID: String, dataService: DataService) { + dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: selectedLabels.map(\.unwrappedID)) + } + func addLabelToItem(_ label: LinkedItemLabel) { selectedLabels.insert(label, at: 0) unselectedLabels.removeAll { $0.name == label.name } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index f9f1e09cc..ca425f081 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -11,6 +11,7 @@ struct WebReaderContainerView: View { @State private var showPreferencesPopover = false @State private var showLabelsModal = false + @State private var showHighlightLabelsModal = false @State private var showTitleEdit = false @State private var showHighlightsView = false @State private var hasPerformedHighlightMutations = false @@ -271,6 +272,9 @@ struct WebReaderContainerView: View { .sheet(isPresented: $showLabelsModal) { ApplyLabelsView(mode: .item(item), onSave: { _ in showLabelsModal = false }) } + .sheet(isPresented: $showHighlightLabelsModal) { + ApplyLabelsView(mode: .item(item), onSave: { _ in showLabelsModal = false }) + } .sheet(isPresented: $showTitleEdit) { LinkedItemMetadataEditView(item: item) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateHighlightLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateHighlightLabelsPublisher.swift new file mode 100644 index 000000000..5f4139652 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateHighlightLabelsPublisher.swift @@ -0,0 +1,72 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func setLabelsForHighlight(highlightID: String, labelIDs: [String]) { + backgroundContext.perform { [weak self] in + guard let self = self else { return } + guard let highlight = Highlight.lookup(byID: highlightID, inContext: self.backgroundContext) else { return } + + if let existingLabels = highlight.labels { + highlight.removeFromLabels(existingLabels) + } + + for labelID in labelIDs { + if let labelObject = LinkedItemLabel.lookup(byID: labelID, inContext: self.backgroundContext) { + highlight.addToLabels(labelObject) + } + } + + // Send update to server + self.syncHighlightLabelUpdates(highlightID: highlightID, labelIDs: labelIDs) + } + } + + func syncHighlightLabelUpdates(highlightID: String, labelIDs: [String]) { + enum MutationResult { + case saved(labels: [InternalLinkedItemLabel]) + case error(errorCode: Enums.SetLabelsErrorCode) + } + + let selection = Selection { + try $0.on( + setLabelsError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }, + setLabelsSuccess: .init { .saved(labels: try $0.labels(selection: highlightLabelSelection.list)) } + ) + } + + let mutation = Selection.Mutation { + try $0.setLabelsForHighlight( + input: InputObjects.SetLabelsForHighlightInput( + highlightId: highlightID, + labelIds: labelIDs + ), + selection: selection + ) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + let context = backgroundContext + + send(mutation, to: path, headers: headers) { result in + let data = try? result.get() + let syncStatus: ServerSyncStatus = data == nil ? .needsUpdate : .isNSync + + context.perform { + guard let highlight = Highlight.lookup(byID: highlightID, inContext: context) else { return } + highlight.serverSyncStatus = Int64(syncStatus.rawValue) + + do { + try context.save() + logger.debug("Highlight labels updated succesfully") + } catch { + context.rollback() + logger.debug("Failed to update highlight labels: \(error.localizedDescription)") + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift index 301f341a0..537521fde 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift @@ -2,7 +2,7 @@ import CoreData import Foundation import Models -struct InternalLinkedItemLabel: Encodable { +public struct InternalLinkedItemLabel: Encodable { let id: String let name: String let color: String @@ -39,7 +39,7 @@ struct InternalLinkedItemLabel: Encodable { return label } - static func make(_ labels: NSSet?) -> [InternalLinkedItemLabel] { + public static func make(_ labels: NSSet?) -> [InternalLinkedItemLabel] { labels? .compactMap { label in if let label = label as? LinkedItemLabel { diff --git a/apple/OmnivoreKit/Sources/Views/Article/OmnivoreWebView.swift b/apple/OmnivoreKit/Sources/Views/Article/OmnivoreWebView.swift index 234da0c9b..b83604f57 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/OmnivoreWebView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/OmnivoreWebView.swift @@ -208,10 +208,12 @@ public final class OmnivoreWebView: WKWebView { // on iOS16 we use menuBuilder to create these items currentMenu = .defaultMenu let annotate = UIMenuItem(title: "Annotate", action: #selector(annotateSelection)) + let labels = UIMenuItem(title: "Labels", action: #selector(setLabels)) + let remove = UIMenuItem(title: "Remove", action: #selector(removeSelection)) // let share = UIMenuItem(title: "Share", action: #selector(shareSelection)) - UIMenuController.shared.menuItems = [remove, /* share, */ annotate] + UIMenuController.shared.menuItems = [remove, labels, annotate] } } @@ -235,6 +237,7 @@ public final class OmnivoreWebView: WKWebView { case #selector(shareSelection): return true case #selector(removeSelection): return true case #selector(copy(_:)): return true + case #selector(setLabels(_:)): return true case Selector(("_lookup:")): return true case Selector(("_define:")): return true case Selector(("_findSelected:")): return true @@ -292,15 +295,18 @@ public final class OmnivoreWebView: WKWebView { hideMenu() } + @objc public func setLabels(_: Any?) {} + override public func buildMenu(with builder: UIMenuBuilder) { if #available(iOS 16.0, *) { let annotate = UICommand(title: "Note", action: #selector(annotateSelection)) let highlight = UICommand(title: "Highlight", action: #selector(highlightSelection)) let remove = UICommand(title: "Remove", action: #selector(removeSelection)) + let setLabels = UICommand(title: "Labels", action: #selector(setLabels)) let omnivore = UIMenu(title: "", options: .displayInline, - children: currentMenu == .defaultMenu ? [highlight, annotate] : [annotate, remove]) + children: currentMenu == .defaultMenu ? [highlight, annotate] : [annotate, setLabels, remove]) builder.insertSibling(omnivore, beforeMenu: .lookup) } diff --git a/apple/OmnivoreKit/Sources/Views/TextChip.swift b/apple/OmnivoreKit/Sources/Views/TextChip.swift index 04e3ebd56..482fb8040 100644 --- a/apple/OmnivoreKit/Sources/Views/TextChip.swift +++ b/apple/OmnivoreKit/Sources/Views/TextChip.swift @@ -78,8 +78,20 @@ public struct TextChip: View { .font(.appCaptionBold) .foregroundColor(textColor) .lineLimit(1) - .background(Capsule().fill(backgroundColor)) - .overlay(Capsule().stroke(borderColor, lineWidth: 1)) +// .background(Capsule().fill(backgroundColor)) +// .overlay(Capsule().stroke(borderColor, lineWidth: 1)) +// .background(Rectangle() +// .stroke(borderColor, lineWidth: 1) +// .background(Rectangle().fill(backgroundColor)) +// .cornerRadius(4) +// ) +// .overlay(Rectangle().stroke(borderColor, lineWidth: 1).cornerRadius(4)) + .background(backgroundColor) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(borderColor, lineWidth: 1) + ) + .padding(1) .overlay(alignment: .topTrailing) { if checked {