From cf3427fdc9542f8a6b463c1792523f5106dc74f1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 26 Oct 2023 10:53:25 +0800 Subject: [PATCH 01/30] WIP: clean up the extension design --- .../Share/Views/ShareExtensionView.swift | 51 +++++++++++++++++++ .../ShareExtensionViewController.swift | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index 2004f9600..16f960923 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -379,6 +379,57 @@ public struct ShareExtensionView: View { } public var body: some View { + VStack(alignment: .leading) { + HStack { + Text("Saved to Omnivore") + .font(Font.system(size: 22, weight: .bold)) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + Button(action: {}, label: { + ZStack { + Circle() + .foregroundColor(Color(hex: "#3D3D3D")) + .frame(width: 30, height: 30) + + Image(systemName: "xmark") + .resizable(resizingMode: Image.ResizingMode.stretch) + .foregroundColor(Color(hex: "#D9D9D9")) + .aspectRatio(contentMode: .fit) + .font(Font.title.weight(.medium)) + .frame(width: 10, height: 10) + } + }) + Button(action: {}, label: { + ZStack { + Circle() + .foregroundColor(Color(hex: "#3D3D3D")) + .frame(width: 30, height: 30) + + Image(systemName: "xmark") + .resizable(resizingMode: Image.ResizingMode.stretch) + .foregroundColor(Color(hex: "#D9D9D9")) + .aspectRatio(contentMode: .fit) + .font(Font.title.weight(.medium)) + .frame(width: 10, height: 10) + } + }) + }.padding(20) + + Spacer() + + HStack { + Spacer() + Button(action: {}, label: { Text("Read Now").font(Font.system(size: 17, weight: .semibold)).padding(20).tint(Color.white) }) + .frame(height: 50) + .background(Color.blue) + .cornerRadius(24) + }.frame(maxWidth: .infinity) + .padding(20) + } + } + + public var oldbody: some View { VStack(alignment: .center) { Capsule() .fill(.gray) diff --git a/apple/Sources/ShareExtension/ShareExtensionViewController.swift b/apple/Sources/ShareExtension/ShareExtensionViewController.swift index c35430eb3..7b1384a64 100644 --- a/apple/Sources/ShareExtension/ShareExtensionViewController.swift +++ b/apple/Sources/ShareExtension/ShareExtensionViewController.swift @@ -12,7 +12,7 @@ import Utils embed( childViewController: UIViewController.makeShareExtensionController(extensionContext: extensionContext), - heightRatio: 0.75 + heightRatio: 0.50 ) } } From 8fe51a5f800cddb646d2ca8f2f3cafedb3d98c2d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 27 Oct 2023 09:56:41 +0800 Subject: [PATCH 02/30] More on ios share extension improvements --- .../Share/ShareExtensionViewModel.swift | 12 +- .../Share/Views/AddNoteSheet.swift | 46 +++ .../Share/Views/ShareExtensionView.swift | 266 +++++++++++++----- .../Sources/Models/PageScrapePayload.swift | 12 +- .../Services/DataService/DataService.swift | 2 +- .../Sources/Views/Colors/Colors.swift | 6 + .../Contents.json | 38 +++ .../Contents.json | 38 +++ .../Contents.json | 38 +++ .../Contents.json | 38 +++ .../Contents.json | 38 +++ .../Sources/Views/SyncingIcon.swift | 2 +- .../ShareExtensionViewController.swift | 20 +- 13 files changed, 477 insertions(+), 79 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonBackground.colorset/Contents.json create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonForeground.colorset/Contents.json create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionBackground.colorset/Contents.json create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index 1602d0421..0dfd96a14 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -9,6 +9,7 @@ public class ShareExtensionViewModel: ObservableObject { @Published public var status: ShareExtensionStatus = .processing @Published public var title: String = "" @Published public var url: String? + @Published public var iconURL: URL? @Published public var highlightData: HighlightData? @Published public var linkedItem: LinkedItem? @Published public var requestId = UUID().uuidString.lowercased() @@ -88,9 +89,10 @@ public class ShareExtensionViewModel: ObservableObject { let hostname = URL(string: payload.url)?.host ?? "" switch payload.contentType { - case let .html(html: _, title: title, highlightData: highlightData): + case let .html(html: _, title: title, iconURL: iconURL, highlightData: highlightData): self.title = title ?? "" self.url = hostname + self.iconURL = iconURL self.highlightData = highlightData case .none: self.url = hostname @@ -145,7 +147,7 @@ public class ShareExtensionViewModel: ObservableObject { localPdfURL: localUrl, url: pageScrapePayload.url ) - case let .html(html, title, _): + case let .html(html, title, _, _): newRequestID = try await services.dataService.createPage( id: requestId, originalHtml: html, @@ -187,7 +189,11 @@ public class ShareExtensionViewModel: ObservableObject { if let title = self.linkedItem?.title { self.title = title } - self.url = self.linkedItem?.pageURLString + if let urlStr = self.linkedItem?.pageURLString, let hostname = URL(string: urlStr)?.host { + self.url = hostname + } else { + self.url = self.linkedItem?.pageURLString + } } } } diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift new file mode 100644 index 000000000..e599eb797 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift @@ -0,0 +1,46 @@ +// +// AddNoteSheet.swift +// +// +// Created by Jackson Harper on 10/26/23. +// + +import Models +import Services +import SwiftUI +import Utils +import Views + +public struct AddNoteSheet: View { + @State var text = "" + + enum FocusField: Hashable { + case noteEditor + } + + @FocusState private var focusedField: FocusField? + + public init() { + UITextView.appearance().textContainerInset = UIEdgeInsets(top: 5, left: 2, bottom: 5, right: 2) + } + + public var body: some View { + NavigationView { + TextEditor(text: $text) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .focused($focusedField, equals: .noteEditor) + .task { + self.focusedField = .noteEditor + } + .background(Color.extensionPanelBackground) + .navigationTitle("Add Note") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(leading: Button(action: {}, label: { + Text("Cancel") + })) + .navigationBarItems(trailing: Button(action: {}, label: { + Text("Save").bold() + })) + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index 16f960923..f1741b345 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -10,8 +10,6 @@ public struct ShareExtensionView: View { @StateObject var labelsViewModel = LabelsViewModel() @StateObject private var viewModel = ShareExtensionViewModel() - @State var reminderTime: ReminderTime? - @State var hideUntilReminded = false @State var previousLabels: [LinkedItemLabel]? @State var messageText: String? @State var showSearchLabels = false @@ -19,6 +17,8 @@ public struct ShareExtensionView: View { @State var viewState = ViewState.mainView @State var showHighlightInstructionAlert = false + @State var showAddNoteModal = false + enum FocusField: Hashable { case titleEditor } @@ -32,16 +32,6 @@ public struct ShareExtensionView: View { @FocusState private var focusedField: FocusField? - private func handleReminderTimeSelection(_ selectedTime: ReminderTime) { - if selectedTime == reminderTime { - reminderTime = nil - hideUntilReminded = false - } else { - reminderTime = selectedTime - hideUntilReminded = true - } - } - private var titleText: String { switch viewModel.status { case .saved, .synced, .syncFailed(error: _): @@ -86,22 +76,22 @@ public struct ShareExtensionView: View { } } - var titleBar: some View { - HStack { - Spacer() - - Image(systemName: "checkmark.circle") - .frame(width: 15, height: 15) - .foregroundColor(.appGreenSuccess) - .opacity(isSynced ? 1.0 : 0.0) - - Text(messageText ?? titleText) - .font(.appSubheadline) - .foregroundColor(titleColor) - - Spacer() - } - } +// var titleBar: some View { +// HStack { +// Spacer() +// +// Image(systemName: "checkmark.circle") +// .frame(width: 15, height: 15) +// .foregroundColor(.appGreenSuccess) +// .opacity(isSynced ? 1.0 : 0.0) +// +// Text(messageText ?? titleText) +// .font(.appSubheadline) +// .foregroundColor(titleColor) +// +// Spacer() +// } +// } public var titleBox: some View { VStack(alignment: .trailing) { @@ -208,7 +198,7 @@ public struct ShareExtensionView: View { } } .padding(viewState == .editingLabels ? 0 : 16) - .background(viewState == .editingLabels ? Color.clear : Color.appButtonBackground) + .background(Color.extensionBackground) .frame(maxWidth: .infinity, maxHeight: viewState == .editingLabels ? .infinity : 60) .cornerRadius(8) } @@ -307,12 +297,12 @@ public struct ShareExtensionView: View { var moreActionsMenu: some View { Menu { - Button( - action: {}, - label: { - Button(LocalText.dismissButton, role: .cancel, action: {}) - } - ) + Button(action: {}, label: { + Label( + "Edit Info", + systemImage: "info.circle" + ) + }) Button(action: { if let linkedItem = self.viewModel.linkedItem { self.viewModel.setLinkArchived(dataService: self.viewModel.services.dataService, @@ -378,55 +368,191 @@ public struct ShareExtensionView: View { viewState = .mainView } - public var body: some View { - VStack(alignment: .leading) { - HStack { - Text("Saved to Omnivore") - .font(Font.system(size: 22, weight: .bold)) + var articleInfoBox: some View { + HStack(alignment: .top, spacing: 15) { + AsyncImage(url: self.viewModel.iconURL) + .frame(width: 56, height: 56).overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(.white, lineWidth: 1) + ).cornerRadius(14) + VStack(alignment: .leading) { + Text(self.viewModel.url ?? "") + .font(Font.system(size: 12)) + .lineLimit(1) + .foregroundColor(Color(hex: "EBEBF5")?.opacity(0.85)) + .frame(height: 14) + + Text(self.viewModel.title) + .font(Font.system(size: 13, weight: .semibold)) + .lineSpacing(1.25) + .foregroundColor(.appGrayTextContrast) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(2) + .frame(height: 33) .frame(maxWidth: .infinity, alignment: .leading) + }.padding(.vertical, 2) + // Spacer() + Image(systemName: "checkmark.circle") + .frame(width: 15, height: 15) + .foregroundColor(.appGreenSuccess) + // .opacity(isSynced ? 1.0 : 0.0) + } + } - Spacer() - Button(action: {}, label: { - ZStack { - Circle() - .foregroundColor(Color(hex: "#3D3D3D")) - .frame(width: 30, height: 30) + var noteBox: some View { + Button(action: { + NotificationCenter.default.post(name: Notification.Name("ExpandForm"), object: nil) + // showAddNoteModal = true + }, label: { Text("Add note...") }) + .foregroundColor(Color.extensionTextSubtle) + .font(Font.system(size: 13, weight: .semibold)) + .frame(height: 50, alignment: .top) + .frame(maxWidth: .infinity, alignment: .leading) + } - Image(systemName: "xmark") - .resizable(resizingMode: Image.ResizingMode.stretch) - .foregroundColor(Color(hex: "#D9D9D9")) - .aspectRatio(contentMode: .fit) - .font(Font.title.weight(.medium)) - .frame(width: 10, height: 10) + var labelsBox: some View { + Button(action: {}, label: { + Label { + Text("Add Labels").font(Font.system(size: 12, weight: .medium)).tint(Color.white) + } icon: { + Image.label.resizable(resizingMode: .stretch).frame(width: 17, height: 17).tint(Color.white) + }.padding(.leading, 10).padding(.trailing, 12) + }) + .frame(height: 28) + .background(Color.blue) + .cornerRadius(24) + } + + var infoBox: some View { + VStack(alignment: .leading, spacing: 15) { + articleInfoBox + + Divider() + .frame(maxWidth: .infinity) + .frame(height: 1) + .background(Color(hex: "545458")?.opacity(0.65)) + + noteBox + + labelsBox + }.padding(15) + .background(Color.extensionPanelBackground) + .cornerRadius(14) + } + + var moreMenuButton: some View { + Menu { + Button(action: {}, label: { + Label( + "Edit Info", + systemImage: "info.circle" + ) + }) + Button(action: { + if let linkedItem = self.viewModel.linkedItem { + self.viewModel.setLinkArchived(dataService: self.viewModel.services.dataService, + objectID: linkedItem.objectID, + archived: true) + messageText = "Link Archived" + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } - }) - Button(action: {}, label: { - ZStack { - Circle() - .foregroundColor(Color(hex: "#3D3D3D")) - .frame(width: 30, height: 30) - - Image(systemName: "xmark") - .resizable(resizingMode: Image.ResizingMode.stretch) - .foregroundColor(Color(hex: "#D9D9D9")) - .aspectRatio(contentMode: .fit) - .font(Font.title.weight(.medium)) - .frame(width: 10, height: 10) + } + }, label: { + Label( + "Archive", + systemImage: "archivebox" + ) + }) + Button( + action: { + if let linkedItem = self.viewModel.linkedItem { + self.viewModel.removeLink(dataService: self.viewModel.services.dataService, objectID: linkedItem.objectID) + messageText = "Link Removed" + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } } - }) - }.padding(20) + }, + label: { + Label("Remove", systemImage: "trash") + } + ) + } label: { + ZStack { + Circle() + .foregroundColor(Color.circleButtonBackground) + .frame(width: 30, height: 30) + + Image(systemName: "ellipsis") + .resizable(resizingMode: Image.ResizingMode.stretch) + .foregroundColor(Color.circleButtonForeground) + .aspectRatio(contentMode: .fit) + .frame(width: 15, height: 15) + } + } + } + + var closeButton: some View { + Button(action: { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + }, label: { + ZStack { + Circle() + .foregroundColor(Color.circleButtonBackground) + .frame(width: 30, height: 30) + + Image(systemName: "xmark") + .resizable(resizingMode: Image.ResizingMode.stretch) + .foregroundColor(Color.circleButtonForeground) + .aspectRatio(contentMode: .fit) + .font(Font.title.weight(.bold)) + .frame(width: 12, height: 12) + } + }) + } + + var titleBar: some View { + HStack { + Text("Saved to Omnivore") + .font(Font.system(size: 22, weight: .bold)) + .frame(maxWidth: .infinity, alignment: .leading) Spacer() + moreMenuButton + closeButton + } + } + + public var body: some View { + VStack(alignment: .leading, spacing: 15) { + titleBar + .padding(.top, 15) + + infoBox + + Spacer(minLength: 1) HStack { Spacer() - Button(action: {}, label: { Text("Read Now").font(Font.system(size: 17, weight: .semibold)).padding(20).tint(Color.white) }) + Button(action: { + viewModel.handleReadNowAction(extensionContext: extensionContext) + }, label: { + Text("Read Now") + .font(Font.system(size: 17, weight: .semibold)) + .tint(Color.white) + .padding(20) + }) .frame(height: 50) .background(Color.blue) .cornerRadius(24) + .padding(15) }.frame(maxWidth: .infinity) - .padding(20) - } + }.padding(.horizontal, 15) + .background(Color.extensionBackground) + .onAppear { + viewModel.savePage(extensionContext: extensionContext) + } } public var oldbody: some View { diff --git a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift index 28004ce07..401a6a1ad 100644 --- a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift +++ b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift @@ -28,7 +28,7 @@ public struct PageScrapePayload { public enum ContentType { case none case pdf(localUrl: URL) - case html(html: String, title: String?, highlightData: HighlightData?) + case html(html: String, title: String?, iconURL: URL?, highlightData: HighlightData?) } public let url: String @@ -49,9 +49,9 @@ public struct PageScrapePayload { self.contentType = .pdf(localUrl: localUrl) } - init(url: String, title: String?, html: String, highlightData: HighlightData?) { + init(url: String, title: String?, html: String, iconURL: URL?, highlightData: HighlightData?) { self.url = url - self.contentType = .html(html: html, title: title, highlightData: highlightData) + self.contentType = .html(html: html, title: title, iconURL: iconURL, highlightData: highlightData) } } @@ -319,6 +319,11 @@ private extension PageScrapePayload { let html = results?["originalHTML"] as? String let title = results?["title"] as? String let contentType = results?["contentType"] as? String + var iconURL: URL? + + if let urlStr = results?["iconURL"] as? String { + iconURL = URL(string: urlStr) + } // If we were not able to capture any HTML, treat this as a URL and // see if the backend can do better. @@ -336,6 +341,7 @@ private extension PageScrapePayload { return PageScrapePayload(url: url, title: title, html: html, + iconURL: iconURL, highlightData: HighlightData.make(dict: results)) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 5e3fe80f1..73f017d58 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -266,7 +266,7 @@ public final class DataService: ObservableObject { linkedItem.contentReader = "PDF" linkedItem.tempPDFURL = localUrl linkedItem.title = PDFUtils.titleFromPdfFile(pageScrape.url) - case let .html(html: html, title: title, highlightData: _): + case let .html(html: html, title: title, iconURL: _, highlightData: _): linkedItem.contentReader = "WEB" linkedItem.originalHtml = html linkedItem.title = title ?? PDFUtils.titleFromPdfFile(pageScrape.url) diff --git a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift index 5fffcf5ac..76f11d506 100644 --- a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift +++ b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift @@ -44,6 +44,12 @@ public extension Color { static var thFeatureSeparator: Color { Color("featureSeparator", bundle: .module) } + static var circleButtonBackground: Color { Color("_circleButtonBackground", bundle: .module) } + static var circleButtonForeground: Color { Color("_circleButtonForeground", bundle: .module) } + static var extensionBackground: Color { Color("_extensionBackground", bundle: .module) } + static var extensionPanelBackground: Color { Color("_extensionPanelBackground", bundle: .module) } + static var extensionTextSubtle: Color { Color("_extensionTextSubtle", bundle: .module) } + // Apple system UIColor equivalents #if os(iOS) static var systemBackground: Color { Color(.systemBackground) } diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonBackground.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonBackground.colorset/Contents.json new file mode 100644 index 000000000..0488699a4 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE9", + "green" : "0xE8", + "red" : "0xE8" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3E", + "green" : "0x3C", + "red" : "0x3B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonForeground.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonForeground.colorset/Contents.json new file mode 100644 index 000000000..2d6f53df7 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_circleButtonForeground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x83", + "green" : "0x81", + "red" : "0x81" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xAB", + "green" : "0xA5", + "red" : "0xA5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionBackground.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionBackground.colorset/Contents.json new file mode 100644 index 000000000..b5472bb11 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF6", + "green" : "0xF6", + "red" : "0xF6" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x20", + "green" : "0x20", + "red" : "0x20" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json new file mode 100644 index 000000000..559b615ae --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xF2", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x30", + "green" : "0x30", + "red" : "0x30" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json new file mode 100644 index 000000000..ac28096a8 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x68", + "green" : "0x69", + "red" : "0x69" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x89", + "green" : "0x89", + "red" : "0x89" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift b/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift index de0dda106..64e2432ba 100644 --- a/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift +++ b/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift @@ -13,7 +13,7 @@ import Utils public struct SyncStatusIcon: View { let status: ServerSyncStatus - init(status: ServerSyncStatus) { + public init(status: ServerSyncStatus) { self.status = status } diff --git a/apple/Sources/ShareExtension/ShareExtensionViewController.swift b/apple/Sources/ShareExtension/ShareExtensionViewController.swift index 7b1384a64..06275285b 100644 --- a/apple/Sources/ShareExtension/ShareExtensionViewController.swift +++ b/apple/Sources/ShareExtension/ShareExtensionViewController.swift @@ -1,20 +1,38 @@ import App +import SwiftUI import Utils +import Views #if os(iOS) import UIKit + final class SheetViewController: UIViewController {} + @objc(ShareExtensionViewController) final class ShareExtensionViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear + NotificationCenter.default.addObserver(forName: Notification.Name("ExpandForm"), object: nil, queue: OperationQueue.main) { _ in + + self.openSheet() + } + embed( childViewController: UIViewController.makeShareExtensionController(extensionContext: extensionContext), - heightRatio: 0.50 + heightRatio: 0.60 ) } + + @IBAction func openSheet() { + let hostingController = UIHostingController(rootView: AddNoteSheet()) + + present(hostingController, animated: true, completion: nil) + + // Present it w/o any adjustments so it uses the default sheet presentation. + // present(sheetViewController., animated: true, completion: nil) + } } #elseif os(macOS) From 9ab9316d76ffe10668bd7af75db6946a124d9b54 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 27 Oct 2023 16:31:29 +0800 Subject: [PATCH 03/30] Update colors --- .../_extensionPanelBackground.colorset/Contents.json | 6 +++--- .../_extensionTextSubtle.colorset/Contents.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json index 559b615ae..d5f1a80a3 100644 --- a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionPanelBackground.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "0xF2", - "green" : "0xF2", - "red" : "0xF2" + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json index ac28096a8..e3b0ba879 100644 --- a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_extensionTextSubtle.colorset/Contents.json @@ -2,12 +2,12 @@ "colors" : [ { "color" : { - "color-space" : "srgb", + "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "0x68", - "green" : "0x69", - "red" : "0x69" + "blue" : "0x89", + "green" : "0x89", + "red" : "0x89" } }, "idiom" : "universal" From 1925ac34e9112fdeb1d36dcd64489d9822f41bdd Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 27 Oct 2023 16:31:47 +0800 Subject: [PATCH 04/30] Snackbar should always be opaque --- .../Sources/App/Views/WebReader/WebReaderContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 040cc4f7c..942ea7cd2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -614,7 +614,7 @@ struct WebReaderContainerView: View { .autohideIn(2) .position(.bottom) .animation(.spring()) - .closeOnTapOutside(true) + .isOpaque(false) } .onReceive(NSNotification.readerSnackBarPublisher) { notification in if let message = notification.userInfo?["message"] as? String { From 92b2f9112bfe63c4833052b3822ad42e0090bdee Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 27 Oct 2023 16:31:53 +0800 Subject: [PATCH 05/30] Fix colors --- .../App/AppExtensions/Share/Views/ShareExtensionView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index f1741b345..7fe76a4b6 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -379,7 +379,7 @@ public struct ShareExtensionView: View { Text(self.viewModel.url ?? "") .font(Font.system(size: 12)) .lineLimit(1) - .foregroundColor(Color(hex: "EBEBF5")?.opacity(0.85)) + .foregroundColor(Color.extensionTextSubtle) .frame(height: 14) Text(self.viewModel.title) From fa82917f552fd54e489c7c884277e71225aa1032 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 10:33:56 +0800 Subject: [PATCH 06/30] Update view models --- .../Share/ShareExtensionScene.swift | 10 +- .../Share/ShareExtensionViewModel.swift | 3 + .../Share/Views/AddNoteSheet.swift | 33 +- .../Share/Views/EditLabelsSheet.swift | 98 +++++ .../Share/Views/ShareExtensionView.swift | 367 ++++++------------ .../App/Views/Labels/LabelsMasonaryView.swift | 1 + .../App/Views/Labels/LabelsViewModel.swift | 4 +- .../Utils/UIViewControllerExtensions.swift | 17 - .../OmnivoreKit/Sources/Views/SearchBar.swift | 2 +- .../ShareExtensionViewController.swift | 34 +- 10 files changed, 279 insertions(+), 290 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 222ca0388..2437a21ed 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -3,11 +3,17 @@ import Utils import Views public extension PlatformViewController { - static func makeShareExtensionController(extensionContext: NSExtensionContext?) -> PlatformViewController { + static func makeShareExtensionController( + viewModel: ShareExtensionViewModel, + labelsViewModel: LabelsViewModel, + extensionContext: NSExtensionContext? + ) -> PlatformViewController { registerFonts() let hostingController = PlatformHostingController( - rootView: ShareExtensionView(extensionContext: extensionContext) + rootView: ShareExtensionView(viewModel: viewModel, + labelsViewModel: labelsViewModel, + extensionContext: extensionContext) ) #if os(iOS) hostingController.view.layer.cornerRadius = 12 diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index 0dfd96a14..37792c953 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -14,10 +14,13 @@ public class ShareExtensionViewModel: ObservableObject { @Published public var linkedItem: LinkedItem? @Published public var requestId = UUID().uuidString.lowercased() @Published var debugText: String? + @Published var noteText: String = "" let services = Services() let queue = OperationQueue() + public init() {} + func handleReadNowAction(extensionContext: NSExtensionContext?) { #if os(iOS) if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift index e599eb797..6a613d386 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift @@ -12,7 +12,11 @@ import Utils import Views public struct AddNoteSheet: View { - @State var text = "" + @Environment(\.dismiss) private var dismiss + + @StateObject var viewModel: ShareExtensionViewModel + let highlightId = UUID().uuidString.lowercased() + let shortId = NanoID.generate(alphabet: NanoID.Alphabet.urlSafe.rawValue, size: 8) enum FocusField: Hashable { case noteEditor @@ -20,13 +24,25 @@ public struct AddNoteSheet: View { @FocusState private var focusedField: FocusField? - public init() { - UITextView.appearance().textContainerInset = UIEdgeInsets(top: 5, left: 2, bottom: 5, right: 2) + public init(viewModel: ShareExtensionViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + UITextView.appearance().textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 10, right: 4) + } + + func saveNote() { + if let linkedItem = viewModel.linkedItem { + _ = viewModel.services.dataService.createNote(shortId: shortId, + highlightID: highlightId, + articleId: linkedItem.unwrappedID, + annotation: viewModel.noteText) + } else { + // Maybe we shouldn't even allow this UI without linkeditem existing + } } public var body: some View { NavigationView { - TextEditor(text: $text) + TextEditor(text: $viewModel.noteText) .frame(maxWidth: .infinity, maxHeight: .infinity) .focused($focusedField, equals: .noteEditor) .task { @@ -35,10 +51,15 @@ public struct AddNoteSheet: View { .background(Color.extensionPanelBackground) .navigationTitle("Add Note") .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(leading: Button(action: {}, label: { + .navigationBarItems(leading: Button(action: { + dismiss() + }, label: { Text("Cancel") })) - .navigationBarItems(trailing: Button(action: {}, label: { + .navigationBarItems(trailing: Button(action: { + saveNote() + dismiss() + }, label: { Text("Save").bold() })) } diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift new file mode 100644 index 000000000..77fe27ff6 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift @@ -0,0 +1,98 @@ +// +// EditLabelsSheet.swift +// +// +// Created by Jackson Harper on 10/27/23. +// + +import Models +import Services +import SwiftUI +import Utils +import Views + +@MainActor +public struct EditLabelsSheet: View { + @State var text = "" + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var dataService: DataService + + @StateObject var labelsViewModel: LabelsViewModel + @StateObject var viewModel: ShareExtensionViewModel + + enum FocusField: Hashable { + case noteEditor + } + + @FocusState private var focusedField: FocusField? + + public init(viewModel: ShareExtensionViewModel, labelsViewModel: LabelsViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + _labelsViewModel = StateObject(wrappedValue: labelsViewModel) + + UITextView.appearance().textContainerInset = UIEdgeInsets(top: 5, left: 2, bottom: 5, right: 2) + } + + func onLabelTap(label: LinkedItemLabel, textChip _: TextChip) { + if labelsViewModel.selectedLabels.contains(label) { + labelsViewModel.selectedLabels.remove(label) + } else { + labelsViewModel.selectedLabels.insert(label) + } + + if let linkedItem = viewModel.linkedItem { + labelsViewModel.saveItemLabelChanges(itemID: linkedItem.unwrappedID, dataService: viewModel.services.dataService) + } + } + + var content: some View { + VStack(spacing: 15) { + SearchBar(searchTerm: $labelsViewModel.labelSearchFilter) + + // swiftlint:disable line_length + ScrollView { + LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter), + selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter), + onLabelTap: onLabelTap) + Button( + action: { labelsViewModel.showCreateLabelModal = true }, + label: { + HStack { + let trimmedLabelName = labelsViewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines) + Image(systemName: "tag").foregroundColor(.blue) + Text( + labelsViewModel.labelSearchFilter.count > 0 ? + "Create: \"\(trimmedLabelName)\" label" : + LocalText.createLabelMessage + ).foregroundColor(.blue) + .font(Font.system(size: 14)) + Spacer() + } + } + ) + .buttonStyle(PlainButtonStyle()) + .padding(10) + } + } + .background(Color.clear) + .padding(20) + } + + public var body: some View { + NavigationView { + content + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.extensionBackground) + .navigationTitle("Set Labels") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: Button(action: { + dismiss() + }, label: { + Text("Done").bold() + })) + } + .task { + await labelsViewModel.loadLabelsFromStore(dataService: viewModel.services.dataService) + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index 7fe76a4b6..a8ff83677 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -7,8 +7,8 @@ import Views // swiftlint:disable file_length type_body_length public struct ShareExtensionView: View { let extensionContext: NSExtensionContext? - @StateObject var labelsViewModel = LabelsViewModel() - @StateObject private var viewModel = ShareExtensionViewModel() + @StateObject var viewModel: ShareExtensionViewModel + @StateObject var labelsViewModel: LabelsViewModel @State var previousLabels: [LinkedItemLabel]? @State var messageText: String? @@ -32,6 +32,15 @@ public struct ShareExtensionView: View { @FocusState private var focusedField: FocusField? + public init(viewModel: ShareExtensionViewModel, + labelsViewModel: LabelsViewModel, + extensionContext: NSExtensionContext?) + { + _viewModel = StateObject(wrappedValue: viewModel) + _labelsViewModel = StateObject(wrappedValue: labelsViewModel) + self.extensionContext = extensionContext + } + private var titleText: String { switch viewModel.status { case .saved, .synced, .syncFailed(error: _): @@ -132,76 +141,76 @@ public struct ShareExtensionView: View { } } - var labelsSection: some View { - HStack { - if viewState != .editingLabels { - ZStack { - Circle() - .foregroundColor(Color.blue) - .frame(width: 34, height: 34) - - Image(systemName: "tag") - .font(.appCallout) - .frame(width: 34, height: 34) - } - .padding(.trailing, 8) - - VStack { - Text(LocalText.labelsGeneric) - .font(.appSubheadline) - .foregroundColor(Color.appGrayTextContrast) - .frame(maxWidth: .infinity, alignment: .leading) - - let labelCount = labelsViewModel.selectedLabels.count - Text(labelCount > 0 ? - "\(labelCount) label\(labelCount > 1 ? "s" : "") selected" - : "Add labels to your saved link") - .font(.appFootnote) - .foregroundColor(Color.appGrayText) - .frame(maxWidth: .infinity, alignment: .leading) - } - - Spacer() - - Image(systemName: "chevron.right") - .font(.appCallout) - } else { - VStack(spacing: 15) { - SearchBar(searchTerm: $labelsViewModel.labelSearchFilter) - - // swiftlint:disable line_length - ScrollView { - LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter), - selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter), - onLabelTap: onLabelTap) - Button( - action: { labelsViewModel.showCreateLabelModal = true }, - label: { - HStack { - let trimmedLabelName = labelsViewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines) - Image(systemName: "tag").foregroundColor(.blue) - Text( - labelsViewModel.labelSearchFilter.count > 0 ? - "Create: \"\(trimmedLabelName)\" label" : - LocalText.createLabelMessage - ).foregroundColor(.blue) - .font(Font.system(size: 14)) - Spacer() - } - } - ) - .buttonStyle(PlainButtonStyle()) - .padding(10) - }.background(Color.appButtonBackground) - // swiftlint:enable line_length - } - } - } - .padding(viewState == .editingLabels ? 0 : 16) - .background(Color.extensionBackground) - .frame(maxWidth: .infinity, maxHeight: viewState == .editingLabels ? .infinity : 60) - .cornerRadius(8) - } +// var labelsSection: some View { +// HStack { +// if viewState != .editingLabels { +// ZStack { +// Circle() +// .foregroundColor(Color.blue) +// .frame(width: 34, height: 34) +// +// Image(systemName: "tag") +// .font(.appCallout) +// .frame(width: 34, height: 34) +// } +// .padding(.trailing, 8) +// +// VStack { +// Text(LocalText.labelsGeneric) +// .font(.appSubheadline) +// .foregroundColor(Color.appGrayTextContrast) +// .frame(maxWidth: .infinity, alignment: .leading) +// +// let labelCount = labelsViewModel.selectedLabels.count +// Text(labelCount > 0 ? +// "\(labelCount) label\(labelCount > 1 ? "s" : "") selected" +// : "Add labels to your saved link") +// .font(.appFootnote) +// .foregroundColor(Color.appGrayText) +// .frame(maxWidth: .infinity, alignment: .leading) +// } +// +// Spacer() +// +// Image(systemName: "chevron.right") +// .font(.appCallout) +// } else { +// VStack(spacing: 15) { +// SearchBar(searchTerm: $labelsViewModel.labelSearchFilter) +// +// // swiftlint:disable line_length +// ScrollView { +// LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter), +// selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter), +// onLabelTap: onLabelTap) +// Button( +// action: { labelsViewModel.showCreateLabelModal = true }, +// label: { +// HStack { +// let trimmedLabelName = labelsViewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines) +// Image(systemName: "tag").foregroundColor(.blue) +// Text( +// labelsViewModel.labelSearchFilter.count > 0 ? +// "Create: \"\(trimmedLabelName)\" label" : +// LocalText.createLabelMessage +// ).foregroundColor(.blue) +// .font(Font.system(size: 14)) +// Spacer() +// } +// } +// ) +// .buttonStyle(PlainButtonStyle()) +// .padding(10) +// }.background(Color.appButtonBackground) +// // swiftlint:enable line_length +// } +// } +// } +// .padding(viewState == .editingLabels ? 0 : 16) +// .background(Color.extensionBackground) +// .frame(maxWidth: .infinity, maxHeight: viewState == .editingLabels ? .infinity : 60) +// .cornerRadius(8) +// } var highlightSection: some View { HStack { @@ -249,52 +258,6 @@ public struct ShareExtensionView: View { .cornerRadius(8) } - func onLabelTap(label: LinkedItemLabel, textChip _: TextChip) { - if labelsViewModel.selectedLabels.contains(label) { - labelsViewModel.selectedLabels.remove(label) - } else { - labelsViewModel.selectedLabels.insert(label) - } - - if let linkedItem = viewModel.linkedItem { - labelsViewModel.saveItemLabelChanges(itemID: linkedItem.unwrappedID, dataService: viewModel.services.dataService) - } - } - - var primaryButtons: some View { - HStack { - Button( - action: { viewModel.handleReadNowAction(extensionContext: extensionContext) }, - label: { - Label("Read Now", systemImage: "book") - .padding(16) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - ) - .foregroundColor(.appGrayTextContrast) - .background(Color.appButtonBackground) - .frame(height: 52) - .cornerRadius(8) - - Spacer(minLength: 8) - - Button( - action: { - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) - }, - label: { - Label(LocalText.readLaterGeneric, systemImage: "text.book.closed.fill") - .padding(16) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - ) - .foregroundColor(.black) - .background(Color.appBackground) - .frame(height: 52) - .cornerRadius(8) - } - } - var moreActionsMenu: some View { Menu { Button(action: {}, label: { @@ -370,11 +333,21 @@ public struct ShareExtensionView: View { var articleInfoBox: some View { HStack(alignment: .top, spacing: 15) { - AsyncImage(url: self.viewModel.iconURL) - .frame(width: 56, height: 56).overlay( - RoundedRectangle(cornerRadius: 14) - .stroke(.white, lineWidth: 1) - ).cornerRadius(14) + AsyncImage(url: self.viewModel.iconURL) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 56, height: 56) + } else { + Color.appButtonBackground + .frame(width: 56, height: 56) + } + } + .frame(width: 56, height: 56).overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(.white, lineWidth: 1) + ).cornerRadius(14) VStack(alignment: .leading) { Text(self.viewModel.url ?? "") .font(Font.system(size: 12)) @@ -399,19 +372,31 @@ public struct ShareExtensionView: View { } } + var hasNoteText: Bool { + !viewModel.noteText.isEmpty + } + var noteBox: some View { Button(action: { - NotificationCenter.default.post(name: Notification.Name("ExpandForm"), object: nil) - // showAddNoteModal = true - }, label: { Text("Add note...") }) - .foregroundColor(Color.extensionTextSubtle) + NotificationCenter.default.post(name: Notification.Name("ShowAddNoteSheet"), object: nil) + }, label: { + Text(hasNoteText ? viewModel.noteText : "Add note...") + .frame(height: 50, alignment: .top) + .frame(maxWidth: .infinity, alignment: .leading) + }) + .foregroundColor(hasNoteText ? + Color.appGrayTextContrast : Color.extensionTextSubtle + ) .font(Font.system(size: 13, weight: .semibold)) .frame(height: 50, alignment: .top) .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } var labelsBox: some View { - Button(action: {}, label: { + Button(action: { + NotificationCenter.default.post(name: Notification.Name("ShowEditLabelsSheet"), object: nil) + }, label: { Label { Text("Add Labels").font(Font.system(size: 12, weight: .medium)).tint(Color.white) } icon: { @@ -546,7 +531,7 @@ public struct ShareExtensionView: View { .frame(height: 50) .background(Color.blue) .cornerRadius(24) - .padding(15) + .padding(.bottom, 15) }.frame(maxWidth: .infinity) }.padding(.horizontal, 15) .background(Color.extensionBackground) @@ -554,128 +539,4 @@ public struct ShareExtensionView: View { viewModel.savePage(extensionContext: extensionContext) } } - - public var oldbody: some View { - VStack(alignment: .center) { - Capsule() - .fill(.gray) - .frame(width: 60, height: 4) - .padding(.top, 10) - - if viewState == .mainView { - titleBar - .padding(.top, 10) - .padding(.bottom, 12) - } else { - ZStack { - Text(editingViewTitle).bold() - .frame(maxWidth: .infinity, alignment: .center) - - Button(action: { - withAnimation { - submitEditTitle() - } - }, label: { Text(LocalText.doneGeneric).bold() }) - .frame(maxWidth: .infinity, alignment: .trailing) - } - .padding(8) - .padding(.bottom, 4) - } - - if viewState == .mainView { - titleBox - } - - if viewState == .editingTitle { - ScrollView(showsIndicators: false) { - VStack(alignment: .center, spacing: 16) { - VStack(alignment: .leading, spacing: 6) { - TextEditor(text: $viewModel.title) - .textFieldStyle(.roundedBorder) - .lineSpacing(6) - .submitLabel(.done) - .accentColor(.appGraySolid) - .foregroundColor(.appGrayTextContrast) - .font(.appSubheadline) - .padding(8) - .background( - RoundedRectangle(cornerRadius: 8) - .strokeBorder(Color.appGrayBorder, lineWidth: 1) - .background(RoundedRectangle(cornerRadius: 8).fill(Color.systemBackground)) - ) - .frame(height: 100) - .focused($focusedField, equals: .titleEditor) - .task { - self.focusedField = .titleEditor - } - .onChange(of: viewModel.title) { text in - if text.last?.isNewline == .some(true) { - viewModel.title.removeLast() - submitEditTitle() - } - } - } - } - .padding(8) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Spacer() - } - - if viewState != .editingTitle { - if viewState != .viewingHighlight { - labelsSection - .onTapGesture { - withAnimation { - previousLabels = Array(self.labelsViewModel.selectedLabels) - viewState = .editingLabels - } - } - } - if viewState != .editingLabels { - highlightSection - .onTapGesture { - withAnimation { - if viewModel.highlightData != nil { - viewState = .viewingHighlight - } else { - showHighlightInstructionAlert = true - } - } - } - } - } - - Spacer() - - if viewState == .mainView { - Divider() - .padding(.bottom, 20) - - primaryButtons - - moreActionsMenu - } - } - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .topLeading - ) - .padding(.horizontal, 16) - .onAppear { - viewModel.savePage(extensionContext: extensionContext) - } - .sheet(isPresented: $labelsViewModel.showCreateLabelModal) { - CreateLabelView(viewModel: labelsViewModel, newLabelName: labelsViewModel.labelSearchFilter) - } - .alert("Before saving an article select text in Safari to create a highlight on save.", - isPresented: $showHighlightInstructionAlert) { - Button(LocalText.genericOk, role: .cancel) { showHighlightInstructionAlert = false } - } - .task { - await labelsViewModel.loadLabelsFromStore(dataService: viewModel.services.dataService) - }.environmentObject(viewModel.services.dataService) - } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsMasonaryView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsMasonaryView.swift index 65c53faa6..19cfef672 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsMasonaryView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsMasonaryView.swift @@ -11,6 +11,7 @@ import SwiftUI import Models import Views +@MainActor struct LabelsMasonaryView: View { var onLabelTap: (LinkedItemLabel, TextChip) -> Void diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 50b11ca6a..fe7c77b05 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -4,7 +4,7 @@ import Services import SwiftUI import Views -@MainActor final class LabelsViewModel: ObservableObject { +@MainActor public final class LabelsViewModel: ObservableObject { let labelNameMaxLength = 64 @Published var isLoading = false @@ -14,6 +14,8 @@ import Views @Published var showCreateLabelModal = false @Published var labelSearchFilter = "" + public init() {} + func setLabels(_ labels: [LinkedItemLabel]) { self.labels = labels.sorted { left, right in let aTrimmed = left.unwrappedName.trimmingCharacters(in: .whitespaces) diff --git a/apple/OmnivoreKit/Sources/Utils/UIViewControllerExtensions.swift b/apple/OmnivoreKit/Sources/Utils/UIViewControllerExtensions.swift index 9b8fcf751..dbb9f2397 100644 --- a/apple/OmnivoreKit/Sources/Utils/UIViewControllerExtensions.swift +++ b/apple/OmnivoreKit/Sources/Utils/UIViewControllerExtensions.swift @@ -48,23 +48,6 @@ child.didMove(toParent: self) } -// -// @objc func keyboardWillShow(notification: Notification) { -// if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { -// if self.view.frame.origin.y == 0{ -// self.view.frame.origin.y -= keyboardSize.height -// } -// } -// -// } -// -// @objc func keyboardWillHide(notification: Notification) { -// if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { -// if self.view.frame.origin.y != 0 { -// self.view.frame.origin.y += keyboardSize.height -// } -// } -// } } #endif diff --git a/apple/OmnivoreKit/Sources/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/Views/SearchBar.swift index e11c75ce5..ae9dc8957 100644 --- a/apple/OmnivoreKit/Sources/Views/SearchBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SearchBar.swift @@ -12,7 +12,7 @@ public struct SearchBar: View { public var body: some View { HStack(spacing: 0) { - TextField("Search", text: $searchTerm) + TextField("Add Labels", text: $searchTerm) .frame(height: 36) .frame(maxWidth: .infinity) .padding(.leading, 28) diff --git a/apple/Sources/ShareExtension/ShareExtensionViewController.swift b/apple/Sources/ShareExtension/ShareExtensionViewController.swift index 06275285b..f72d5798f 100644 --- a/apple/Sources/ShareExtension/ShareExtensionViewController.swift +++ b/apple/Sources/ShareExtension/ShareExtensionViewController.swift @@ -1,4 +1,5 @@ import App +import Services import SwiftUI import Utils import Views @@ -6,32 +7,45 @@ import Views #if os(iOS) import UIKit - final class SheetViewController: UIViewController {} - @objc(ShareExtensionViewController) final class ShareExtensionViewController: UIViewController { + let labelsViewModel = LabelsViewModel() + let viewModel = ShareExtensionViewModel() + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear - NotificationCenter.default.addObserver(forName: Notification.Name("ExpandForm"), object: nil, queue: OperationQueue.main) { _ in + NotificationCenter.default.addObserver( + forName: Notification.Name("ShowAddNoteSheet"), + object: nil, + queue: OperationQueue.main + ) { _ in + self.openSheet(AnyView(AddNoteSheet(viewModel: self.viewModel))) + } - self.openSheet() + NotificationCenter.default.addObserver( + forName: Notification.Name("ShowEditLabelsSheet"), + object: nil, + queue: OperationQueue.main + ) { _ in + self.openSheet(AnyView(EditLabelsSheet(viewModel: self.viewModel, labelsViewModel: self.labelsViewModel))) } embed( - childViewController: UIViewController.makeShareExtensionController(extensionContext: extensionContext), + childViewController: UIViewController.makeShareExtensionController( + viewModel: viewModel, + labelsViewModel: labelsViewModel, + extensionContext: extensionContext + ), heightRatio: 0.60 ) } - @IBAction func openSheet() { - let hostingController = UIHostingController(rootView: AddNoteSheet()) + func openSheet(_ rootView: AnyView) { + let hostingController = UIHostingController(rootView: rootView) present(hostingController, animated: true, completion: nil) - - // Present it w/o any adjustments so it uses the default sheet presentation. - // present(sheetViewController., animated: true, completion: nil) } } From bd3d8b1b9d5d1d6b546b5eedf8973860aeb64d41 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 11:25:18 +0800 Subject: [PATCH 07/30] Add intercomHash GQL definition --- .../Services/DataService/GQLSchema.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index 32e46f8f0..e5df26be6 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -23024,6 +23024,7 @@ extension Objects { let followersCount: [String: Int] let friendsCount: [String: Int] let id: [String: String] + let intercomHash: [String: String] let isFriend: [String: Bool] let isFullUser: [String: Bool] let name: [String: String] @@ -23070,6 +23071,10 @@ extension Objects.User: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "intercomHash": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "isFriend": if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -23128,6 +23133,7 @@ extension Objects.User: Decodable { followersCount = map["followersCount"] friendsCount = map["friendsCount"] id = map["id"] + intercomHash = map["intercomHash"] isFriend = map["isFriend"] isFullUser = map["isFullUser"] name = map["name"] @@ -23206,6 +23212,21 @@ extension Fields where TypeLock == Objects.User { } } + func intercomHash() throws -> String? { + let field = GraphQLField.leaf( + name: "intercomHash", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.intercomHash[field.alias!] + case .mocking: + return nil + } + } + @available(*, deprecated, message: "isFriend has been replaced with viewerIsFollowing") func isFriend() throws -> Bool? { let field = GraphQLField.leaf( From 08f9beab68200c1c60a403d3b507dc1982e1a82f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 11:25:50 +0800 Subject: [PATCH 08/30] Improve site icon fetching --- apple/Sources/ShareExtension/ShareExtension.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apple/Sources/ShareExtension/ShareExtension.js b/apple/Sources/ShareExtension/ShareExtension.js index 4581ece82..dbbdab9c9 100644 --- a/apple/Sources/ShareExtension/ShareExtension.js +++ b/apple/Sources/ShareExtension/ShareExtension.js @@ -2,10 +2,16 @@ var ShareExtension = function() {}; const iconURL = () => { try { - const previewImage = document.querySelector("meta[property='og:image'], meta[name='twitter:image']").content - if (previewImage) { return previewImage } + const previewImage = document.querySelector("meta[property='og:image'], meta[name='twitter:image']") + if (previewImage && previewImage.getAttribute("content")) { return previewImage.getAttribute("content") } - return document.querySelector("link[rel='apple-touch-icon'], link[rel='shortcut icon'], link[rel='icon']").href + const appleImage = document.querySelector("link[rel='apple-touch-icon'], link[rel='shortcut icon'], link[rel='icon']") + if (appleImage && appleImage.getAttribute("href")) { return appleImage.getAttribute("href") } + + const href = new URL(document.location.href) + href.pathname = '/favicon.ico' + + return href.toString() } catch {} return undefined } From 28393974fb0d657dcb2384096a2050325dd10a45 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 15:33:26 +0800 Subject: [PATCH 09/30] Filter out inactive subscriptions on iOS --- apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index bc381d013..00adc5c4a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -14,7 +14,7 @@ import Views isLoading = true do { - subscriptions = try await dataService.subscriptions() + subscriptions = try await dataService.subscriptions().filter { $0.status == SubscriptionStatus.active } } catch { hasNetworkError = true } From ba5ad51e6a126fb64653d03d972576b125b6b2c1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 15:34:25 +0800 Subject: [PATCH 10/30] Set intercom user hash --- .../Sources/App/Views/LibraryTabView.swift | 1 + .../App/Views/Profile/ProfileView.swift | 24 +++++----- .../Sources/App/Views/RootView/RootView.swift | 1 + .../App/Views/RootView/RootViewModel.swift | 3 ++ .../Services/DataService/DataService.swift | 15 +----- .../DataService/Queries/ViewerFetcher.swift | 46 +++++++++++-------- apple/Sources/MainApp.swift | 1 + 7 files changed, 46 insertions(+), 45 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 9e0739417..763bef4f6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -12,6 +12,7 @@ import Services import SwiftUI import Views +@MainActor struct LibraryTabView: View { @EnvironmentObject var dataService: DataService diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index e47f3682e..3d46b8672 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -18,17 +18,15 @@ import Views } func loadProfileData(dataService: DataService) async { - if let currentViewer = dataService.currentViewer { - loadProfileCardData(viewer: currentViewer) - return + if let currentViewer = dataService.currentViewer, + let name = currentViewer.name, + let username = currentViewer.username + { + loadProfileCardData(name: name, username: username, profileImageURL: currentViewer.profileImageURL) } - guard let viewerObjectID = try? await dataService.fetchViewer() else { return } - - await dataService.viewContext.perform { - if let viewer = dataService.viewContext.object(with: viewerObjectID) as? Viewer { - self.loadProfileCardData(viewer: viewer) - } + if let viewer = try? await dataService.fetchViewer() { + loadProfileCardData(name: viewer.name, username: viewer.username, profileImageURL: viewer.profileImageURL) } } @@ -47,11 +45,11 @@ import Views } } - private func loadProfileCardData(viewer: Viewer) { + private func loadProfileCardData(name: String, username: String, profileImageURL: String?) { profileCardData = ProfileCardData( - name: viewer.unwrappedName, - username: viewer.unwrappedUsername, - imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } + name: name, + username: username, + imageURL: profileImageURL.flatMap { URL(string: $0) } ) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index abff913d1..c48252a58 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -16,6 +16,7 @@ public struct RootView: View { if let intercomProvider = intercomProvider { DataService.showIntercomMessenger = intercomProvider.showIntercomMessenger DataService.registerIntercomUser = intercomProvider.registerIntercomUser + DataService.setIntercomUserHash = intercomProvider.setIntercomUserHash Authenticator.unregisterIntercomUser = intercomProvider.unregisterIntercomUser } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index 24796a89f..2865d1887 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -43,15 +43,18 @@ public final class RootViewModel: ObservableObject { public struct IntercomProvider { public init( registerIntercomUser: @escaping (String) -> Void, + setIntercomUserHash: @escaping (String) -> Void, unregisterIntercomUser: @escaping () -> Void, showIntercomMessenger: @escaping () -> Void ) { self.registerIntercomUser = registerIntercomUser + self.setIntercomUserHash = setIntercomUserHash self.unregisterIntercomUser = unregisterIntercomUser self.showIntercomMessenger = showIntercomMessenger } public let registerIntercomUser: (String) -> Void + public let setIntercomUserHash: (String) -> Void public let unregisterIntercomUser: () -> Void public let showIntercomMessenger: () -> Void } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 73f017d58..880a88a44 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -17,6 +17,8 @@ let logger = Logger(subsystem: "app.omnivore", category: "data-service") public final class DataService: ObservableObject { public static var registerIntercomUser: ((String) -> Void)? + public static var setIntercomUserHash: ((String) -> Void)? + public static var showIntercomMessenger: (() -> Void)? public let appEnvironment: AppEnvironment @@ -96,19 +98,6 @@ public final class DataService: ObservableObject { return try? persistentContainer.viewContext.fetch(fetchRequest).first } - public func username() async -> String? { - if let cachedUsername = currentViewer?.username { - return cachedUsername - } - - if let viewerObjectID = try? await fetchViewer() { - let viewer = backgroundContext.object(with: viewerObjectID) as? Viewer - return viewer?.unwrappedUsername - } - - return nil - } - public func switchAppEnvironment(appEnvironment: AppEnvironment) { do { try ValetKey.appEnvironmentString.setValue(appEnvironment.rawValue) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift index 28f8668d2..78117f8a0 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift @@ -5,7 +5,8 @@ import SwiftGraphQL import Utils public extension DataService { - func fetchViewer() async throws -> NSManagedObjectID { + @MainActor + func fetchViewer() async throws -> ViewerInternal? { let selection = Selection { ViewerInternal( userID: try $0.id(), @@ -15,7 +16,8 @@ public extension DataService { name: try $0.name(), profileImageURL: try $0.profile( selection: .init { try $0.pictureUrl() } - ) + ), + intercomHash: try $0.intercomHash() ) } @@ -29,15 +31,24 @@ public extension DataService { return try await withCheckedThrowingContinuation { continuation in send(query, to: path, headers: headers) { [weak self] result in switch result { - case let .success(payload): + case let .success(payload: payload): if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil { UserDefaults.standard.setValue(payload.data.userID, forKey: Keys.userIdKey) DataService.registerIntercomUser?(payload.data.userID) } - if let self = self, let viewerID = payload.data.persist(context: self.backgroundContext) { - continuation.resume(returning: viewerID) - } else { + do { + if let intercomUserHash = payload.data.intercomHash { + DataService.setIntercomUserHash?(intercomUserHash) + } + + if let self = self { + try payload.data.persist(context: self.backgroundContext) + continuation.resume(returning: payload.data) + } else { + continuation.resume(throwing: BasicError.message(messageText: "no self found")) + } + } catch { continuation.resume(throwing: BasicError.message(messageText: "coredata error")) } case .failure: @@ -48,16 +59,15 @@ public extension DataService { } } -private struct ViewerInternal { - let userID: String - let username: String - let name: String - let profileImageURL: String? +public struct ViewerInternal { + public let userID: String + public let username: String + public let name: String + public let profileImageURL: String? + public let intercomHash: String? - func persist(context: NSManagedObjectContext) -> NSManagedObjectID? { - var objectID: NSManagedObjectID? - - context.performAndWait { + func persist(context: NSManagedObjectContext) throws { + try context.performAndWait { let viewer = Viewer(context: context) viewer.userID = userID viewer.username = username @@ -66,15 +76,13 @@ private struct ViewerInternal { do { try context.save() + EventTracker.registerUser(userID: userID) logger.debug("Viewer saved succesfully") - objectID = viewer.objectID - EventTracker.registerUser(userID: viewer.unwrappedUserID) } catch { context.rollback() logger.debug("Failed to save Viewer: \(error.localizedDescription)") + throw error } } - - return objectID } } diff --git a/apple/Sources/MainApp.swift b/apple/Sources/MainApp.swift index a5dc0dc0e..8f7151a84 100644 --- a/apple/Sources/MainApp.swift +++ b/apple/Sources/MainApp.swift @@ -30,6 +30,7 @@ struct MainApp: App { RootView( intercomProvider: AppKeys.sharedInstance?.intercom != nil ? IntercomProvider( registerIntercomUser: { Intercom.registerUser(withUserId: $0) }, + setIntercomUserHash: { Intercom.setUserHash($0) }, unregisterIntercomUser: Intercom.logout, showIntercomMessenger: Intercom.presentMessenger ) : nil From 77c69043bc16950c39c7ef49951a931b9281aa10 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 15:40:58 +0800 Subject: [PATCH 11/30] Remove some old code --- .../Share/Views/ShareExtensionView.swift | 271 +----------------- 1 file changed, 1 insertion(+), 270 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index a8ff83677..5d46fea5c 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -34,35 +34,12 @@ public struct ShareExtensionView: View { public init(viewModel: ShareExtensionViewModel, labelsViewModel: LabelsViewModel, - extensionContext: NSExtensionContext?) - { + extensionContext: NSExtensionContext?) { _viewModel = StateObject(wrappedValue: viewModel) _labelsViewModel = StateObject(wrappedValue: labelsViewModel) self.extensionContext = extensionContext } - private var titleText: String { - switch viewModel.status { - case .saved, .synced, .syncFailed(error: _): - return "Saved to Omnivore" - case .processing: - return "Saving to Omnivore" - case .failed(error: _): - return "Error saving to Omnivore" - } - } - - private var titleColor: Color { - switch viewModel.status { - case .saved, .processing: - return .appGrayText - case .failed(error: _), .syncFailed(error: _): - return .red - case .synced: - return .appGreenSuccess - } - } - private func localImage(from url: URL) -> Image? { #if os(iOS) if let data = try? Data(contentsOf: url), let img = UIImage(data: data) { @@ -85,252 +62,6 @@ public struct ShareExtensionView: View { } } -// var titleBar: some View { -// HStack { -// Spacer() -// -// Image(systemName: "checkmark.circle") -// .frame(width: 15, height: 15) -// .foregroundColor(.appGreenSuccess) -// .opacity(isSynced ? 1.0 : 0.0) -// -// Text(messageText ?? titleText) -// .font(.appSubheadline) -// .foregroundColor(titleColor) -// -// Spacer() -// } -// } - - public var titleBox: some View { - VStack(alignment: .trailing) { - Button(action: {}, label: { - Text("Edit") - .font(.appFootnote) - .padding(.trailing, 8) - .onTapGesture { - viewState = .editingTitle - } - }) - .disabled(viewState == .editingTitle) - .opacity(viewState == .editingTitle ? 0.0 : 1.0) - - VStack(alignment: .leading) { - if viewState != .editingTitle { - Text(self.viewModel.title) - .font(.appSubheadline) - .lineLimit(2) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(.appGrayTextContrast) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - Text(self.viewModel.url ?? "") - .font(.appFootnote) - .foregroundColor(.appGrayText) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - .frame(maxWidth: .infinity, maxHeight: 60) - .padding() - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.appGrayBorder, lineWidth: 1) - ) - } - } - -// var labelsSection: some View { -// HStack { -// if viewState != .editingLabels { -// ZStack { -// Circle() -// .foregroundColor(Color.blue) -// .frame(width: 34, height: 34) -// -// Image(systemName: "tag") -// .font(.appCallout) -// .frame(width: 34, height: 34) -// } -// .padding(.trailing, 8) -// -// VStack { -// Text(LocalText.labelsGeneric) -// .font(.appSubheadline) -// .foregroundColor(Color.appGrayTextContrast) -// .frame(maxWidth: .infinity, alignment: .leading) -// -// let labelCount = labelsViewModel.selectedLabels.count -// Text(labelCount > 0 ? -// "\(labelCount) label\(labelCount > 1 ? "s" : "") selected" -// : "Add labels to your saved link") -// .font(.appFootnote) -// .foregroundColor(Color.appGrayText) -// .frame(maxWidth: .infinity, alignment: .leading) -// } -// -// Spacer() -// -// Image(systemName: "chevron.right") -// .font(.appCallout) -// } else { -// VStack(spacing: 15) { -// SearchBar(searchTerm: $labelsViewModel.labelSearchFilter) -// -// // swiftlint:disable line_length -// ScrollView { -// LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter), -// selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter), -// onLabelTap: onLabelTap) -// Button( -// action: { labelsViewModel.showCreateLabelModal = true }, -// label: { -// HStack { -// let trimmedLabelName = labelsViewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines) -// Image(systemName: "tag").foregroundColor(.blue) -// Text( -// labelsViewModel.labelSearchFilter.count > 0 ? -// "Create: \"\(trimmedLabelName)\" label" : -// LocalText.createLabelMessage -// ).foregroundColor(.blue) -// .font(Font.system(size: 14)) -// Spacer() -// } -// } -// ) -// .buttonStyle(PlainButtonStyle()) -// .padding(10) -// }.background(Color.appButtonBackground) -// // swiftlint:enable line_length -// } -// } -// } -// .padding(viewState == .editingLabels ? 0 : 16) -// .background(Color.extensionBackground) -// .frame(maxWidth: .infinity, maxHeight: viewState == .editingLabels ? .infinity : 60) -// .cornerRadius(8) -// } - - var highlightSection: some View { - HStack { - if viewState != .viewingHighlight { - ZStack { - Circle() - .foregroundColor(Color.appBackground) - .frame(width: 34, height: 34) - - Image(systemName: "highlighter") - .font(.appCallout) - .frame(width: 34, height: 34) - .foregroundColor(Color.black) - } - .padding(.trailing, 8) - - VStack { - Text(LocalText.genericHighlight) - .font(.appSubheadline) - .foregroundColor(Color.appGrayTextContrast) - .frame(maxWidth: .infinity, alignment: .leading) - - Text(viewModel.highlightData != nil ? - viewModel.highlightData!.highlightText - : "Select text before saving to create highlight") - .font(.appFootnote) - .foregroundColor(Color.appGrayText) - .frame(maxWidth: .infinity, alignment: .leading) - } - - Spacer() - - Image(systemName: "chevron.right") - .font(.appCallout) - } else if let highlightText = self.viewModel.highlightData?.highlightText { - Text(highlightText) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .cornerRadius(8) - .padding(0) - } - } - .padding(16) - .frame(maxWidth: .infinity, maxHeight: viewState == .viewingHighlight ? .infinity : 60) - .background(Color.appButtonBackground) - .cornerRadius(8) - } - - var moreActionsMenu: some View { - Menu { - Button(action: {}, label: { - Label( - "Edit Info", - systemImage: "info.circle" - ) - }) - Button(action: { - if let linkedItem = self.viewModel.linkedItem { - self.viewModel.setLinkArchived(dataService: self.viewModel.services.dataService, - objectID: linkedItem.objectID, - archived: true) - messageText = "Link Archived" - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) - } - } - }, label: { - Label( - "Archive", - systemImage: "archivebox" - ) - }) - Button( - action: { - if let linkedItem = self.viewModel.linkedItem { - self.viewModel.removeLink(dataService: self.viewModel.services.dataService, objectID: linkedItem.objectID) - messageText = "Link Removed" - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) - } - } - }, - label: { - Label("Remove", systemImage: "trash") - } - ) - } label: { - Text("More Actions") - .font(.appFootnote) - .foregroundColor(Color.blue) - .frame(maxWidth: .infinity) - .padding(8) - .padding(.bottom, 8) - } - } - - var editingViewTitle: String { - switch viewState { - case .editingTitle: - return "Edit Title" - case .editingLabels: - return LocalText.labelsGeneric - case .viewingHighlight: - return LocalText.genericHighlight - default: - return "" - } - } - - func submitEditTitle() { - if viewState == .editingTitle { - if let linkedItem = viewModel.linkedItem { - viewModel.submitTitleEdit(dataService: viewModel.services.dataService, - itemID: linkedItem.unwrappedID, - title: viewModel.title, - description: linkedItem.descriptionText ?? "") - } - } - viewState = .mainView - } - var articleInfoBox: some View { HStack(alignment: .top, spacing: 15) { AsyncImage(url: self.viewModel.iconURL) { phase in From 27632c6924110d6b9401b349bbd0217e3406e3b8 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 18:44:13 +0800 Subject: [PATCH 12/30] Some scaffolding for multiselect --- .../Share/Views/ShareExtensionView.swift | 3 +- .../Components/FeedCardNavigationLink.swift | 2 +- .../App/Views/Home/HomeFeedViewIOS.swift | 152 ++++++++++-------- .../App/Views/Home/HomeFeedViewModel.swift | 1 + .../Views/FeedItem/LibraryItemCard.swift | 15 ++ 5 files changed, 107 insertions(+), 66 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index 5d46fea5c..715db6ea5 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -34,7 +34,8 @@ public struct ShareExtensionView: View { public init(viewModel: ShareExtensionViewModel, labelsViewModel: LabelsViewModel, - extensionContext: NSExtensionContext?) { + extensionContext: NSExtensionContext?) + { _viewModel = StateObject(wrappedValue: viewModel) _labelsViewModel = StateObject(wrappedValue: labelsViewModel) self.extensionContext = extensionContext diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 37865d853..ad02df3d8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -32,7 +32,7 @@ struct FeedCardNavigationLink: View { @EnvironmentObject var audioController: AudioController let item: LinkedItem - + let isInMultiSelectMode: Bool @ObservedObject var viewModel: HomeFeedViewModel var body: some View { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index dba244997..7a79983f4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -86,70 +86,7 @@ struct AnimatingCellHeight: AnimatableModifier { } // .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .barLeading) { - VStack(alignment: .leading) { - let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName - - Text(title) - .font(Font.system(size: isListScrolled ? 10 : 18, weight: .semibold)) - - if prefersListLayout, isListScrolled { - Text(listTitle) - .font(Font.system(size: 15, weight: .regular)) - .foregroundColor(Color.appGrayText) - } - }.frame(maxWidth: .infinity, alignment: .leading) - } - ToolbarItem(placement: .barTrailing) { - Button("", action: {}) - .disabled(true) - .overlay { - if viewModel.isLoading, !prefersListLayout, enableGrid { - ProgressView() - } - } - } - ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) { - if enableGrid { - Button( - action: { prefersListLayout.toggle() }, - label: { - Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") - } - ) - } else { - EmptyView() - } - } - ToolbarItem(placement: .barTrailing) { - Button( - action: { searchPresented = true }, - label: { - Image(systemName: "magnifyingglass") - .resizable() - .frame(width: 18, height: 18) - .padding(.vertical) - .foregroundColor(.appGrayTextContrast) - } - ) - } - ToolbarItem(placement: .barTrailing) { - if UIDevice.isIPhone { - Menu(content: { - Button(action: { settingsPresented = true }, label: { - Label(LocalText.genericProfile, systemImage: "person.circle") - }) - Button(action: { addLinkPresented = true }, label: { - Label("Add Link", systemImage: "plus.circle") - }) - }, label: { - Image.utilityMenu - }) - .foregroundColor(.appGrayTextContrast) - } else { - EmptyView() - } - } + toolbarItems } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in loadItems(isRefresh: false) @@ -215,6 +152,92 @@ struct AnimatingCellHeight: AnimatableModifier { } } } + + var toolbarItems: some ToolbarContent { + Group { + ToolbarItem(placement: .barLeading) { + VStack(alignment: .leading) { + let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName + + Text(title) + .font(Font.system(size: isListScrolled ? 10 : 18, weight: .semibold)) + + if prefersListLayout, isListScrolled { + Text(listTitle) + .font(Font.system(size: 15, weight: .regular)) + .foregroundColor(Color.appGrayText) + } + }.frame(maxWidth: .infinity, alignment: .leading) + } + ToolbarItem(placement: .barTrailing) { + Button("", action: {}) + .disabled(true) + .overlay { + if viewModel.isLoading, !prefersListLayout, enableGrid { + ProgressView() + } + } + } + ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) { + if enableGrid { + Button( + action: { prefersListLayout.toggle() }, + label: { + Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") + } + ) + } else { + EmptyView() + } + } + ToolbarItem(placement: .barTrailing) { + Button( + action: { searchPresented = true }, + label: { + Image(systemName: "magnifyingglass") + .resizable() + .frame(width: 18, height: 18) + .padding(.vertical) + .foregroundColor(.appGrayTextContrast) + } + ) + } + ToolbarItem(placement: .barTrailing) { + if UIDevice.isIPhone { + Menu(content: { +// Button(action: { +// // withAnimation { +// viewModel.isInMultiSelectMode.toggle() +// // } +// }, label: { +// Label(viewModel.isInMultiSelectMode ? "End Multiselect" : "Select Multiple", systemImage: "checkmark.circle") +// }) + Button(action: { addLinkPresented = true }, label: { + Label("Add Link", systemImage: "plus.circle") + }) + Button(action: { settingsPresented = true }, label: { + Label(LocalText.genericProfile, systemImage: "person.circle") + }) + + }, label: { + Image.utilityMenu + }) + .foregroundColor(.appGrayTextContrast) + } else { + EmptyView() + } + } +// if viewModel.isInMultiSelectMode { +// ToolbarItemGroup(placement: .bottomBar) { +// Button(action: {}, label: { Image(systemName: "archivebox") }) +// Button(action: {}, label: { Image(systemName: "trash") }) +// Button(action: {}, label: { Image.label }) +// Spacer() +// Button(action: { viewModel.isInMultiSelectMode = false }, label: { Text("Cancel") }) +// } +// } + } + } } @MainActor @@ -531,6 +554,7 @@ struct AnimatingCellHeight: AnimatableModifier { ForEach(viewModel.items) { item in FeedCardNavigationLink( item: item, + isInMultiSelectMode: viewModel.isInMultiSelectMode, viewModel: viewModel ) .background(GeometryReader { geometry in diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index c39745961..d1ba61cbc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -24,6 +24,7 @@ import Views @Published var itemToSnoozeID: String? @Published var linkRequest: LinkRequest? @Published var showLoadingBar = false + @Published var isInMultiSelectMode = false @Published var appliedSort = LinkedItemSort.newest.rawValue @Published var selectedLinkItem: NSManagedObjectID? // used by mac app only diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index 83350cd70..47b41f3cf 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -281,3 +281,18 @@ public struct LibraryItemCard: View { LabelsFlowLayout(labels: nonFlairLabels) } } + +struct CircleCheckboxToggleStyle: ToggleStyle { + func makeBody(configuration: Configuration) -> some View { + Button(action: { + configuration.isOn.toggle() + }, label: { + HStack { + Image(systemName: configuration.isOn ? "checkmark.circle" : "circle") + .font(Font.system(size: 18)) + .foregroundColor(configuration.isOn ? Color.blue : Color.appGrayTextContrast) + } + }) + .buttonStyle(.plain) + } +} From 57c481ccfdf1ef913c16577c29f29cb4f9f24f19 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 Oct 2023 19:16:47 +0800 Subject: [PATCH 13/30] Add editinfo to share extension --- .../Share/Views/AddNoteSheet.swift | 2 +- .../Share/Views/EditInfoSheet.swift | 50 +++++++++++++++++++ .../Share/Views/ShareExtensionView.swift | 4 +- .../Views/LinkedItemMetadataEditView.swift | 6 +-- .../ShareExtensionViewController.swift | 8 +++ 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditInfoSheet.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift index 6a613d386..335f53cc2 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/AddNoteSheet.swift @@ -48,7 +48,7 @@ public struct AddNoteSheet: View { .task { self.focusedField = .noteEditor } - .background(Color.extensionPanelBackground) + .background(Color.extensionBackground) .navigationTitle("Add Note") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(leading: Button(action: { diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditInfoSheet.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditInfoSheet.swift new file mode 100644 index 000000000..45dea726b --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditInfoSheet.swift @@ -0,0 +1,50 @@ +// +// EditInfoSheet.swift +// +// +// Created by Jackson Harper on 10/30/23. +// + +import Models +import Services +import SwiftUI +import Utils +import Views + +public struct EditInfoSheet: View { + @Environment(\.dismiss) private var dismiss + + @StateObject var viewModel: ShareExtensionViewModel + let highlightId = UUID().uuidString.lowercased() + let shortId = NanoID.generate(alphabet: NanoID.Alphabet.urlSafe.rawValue, size: 8) + + enum FocusField: Hashable { + case noteEditor + } + + @FocusState private var focusedField: FocusField? + + public init(viewModel: ShareExtensionViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + UITextView.appearance().textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 10, right: 4) + } + +// func saveInfo() { +// if let linkedItem = viewModel.linkedItem { +// _ = viewModel.services.dataService.updateLinkedItemTitleAndDescription(itemID: linkedItem.unwrappedID, title: title, description: description, author: author) +// } else { +// // Maybe we shouldn't even allow this UI without linkeditem existing +// } +// } + + public var body: some View { + if let item = viewModel.linkedItem { + LinkedItemMetadataEditView(item: item) { title, _ in + viewModel.title = title + } + .environmentObject(viewModel.services.dataService) + } else { + ProgressView() + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index 715db6ea5..8954e26b1 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -159,7 +159,9 @@ public struct ShareExtensionView: View { var moreMenuButton: some View { Menu { - Button(action: {}, label: { + Button(action: { + NotificationCenter.default.post(name: Notification.Name("ShowEditInfoSheet"), object: nil) + }, label: { Label( "Edit Info", systemImage: "info.circle" diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift index d7f81aef7..6258fee2f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift @@ -95,13 +95,13 @@ struct LinkedItemMetadataEditView: View { var iOSBody: some View { NavigationView { editForm - .navigationTitle("Edit Title and Description") + .navigationTitle("Edit Info") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .barLeading) { Button( action: { presentationMode.wrappedValue.dismiss() }, - label: { Text(LocalText.cancelGeneric).foregroundColor(.appGrayTextContrast) } + label: { Text(LocalText.cancelGeneric) } ) } ToolbarItem(placement: .barTrailing) { @@ -113,7 +113,7 @@ struct LinkedItemMetadataEditView: View { } presentationMode.wrappedValue.dismiss() }, - label: { Text(LocalText.genericSave).foregroundColor(.appGrayTextContrast) } + label: { Text(LocalText.genericSave).bold() } ) } } diff --git a/apple/Sources/ShareExtension/ShareExtensionViewController.swift b/apple/Sources/ShareExtension/ShareExtensionViewController.swift index f72d5798f..73c8f6242 100644 --- a/apple/Sources/ShareExtension/ShareExtensionViewController.swift +++ b/apple/Sources/ShareExtension/ShareExtensionViewController.swift @@ -32,6 +32,14 @@ import Views self.openSheet(AnyView(EditLabelsSheet(viewModel: self.viewModel, labelsViewModel: self.labelsViewModel))) } + NotificationCenter.default.addObserver( + forName: Notification.Name("ShowEditInfoSheet"), + object: nil, + queue: OperationQueue.main + ) { _ in + self.openSheet(AnyView(EditInfoSheet(viewModel: self.viewModel))) + } + embed( childViewController: UIViewController.makeShareExtensionController( viewModel: viewModel, From 3be2db1113857d9c4a601b41eb5bfe7cda9b23d7 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Nov 2023 12:41:30 +0800 Subject: [PATCH 14/30] Improvements to label editor --- .../Share/Views/EditLabelsSheet.swift | 22 +- .../App/Views/Labels/ApplyLabelsView.swift | 18 +- .../App/Views/Labels/LabelsViewModel.swift | 9 +- .../Sources/App/Views/SearchBar.swift | 200 ++++++++++++++++++ .../Sources/Views/Colors/Colors.swift | 2 + .../Contents.json | 38 ++++ .../OmnivoreKit/Sources/Views/SearchBar.swift | 59 ------ 7 files changed, 274 insertions(+), 74 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/SearchBar.swift create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_textFieldBackground.colorset/Contents.json delete mode 100644 apple/OmnivoreKit/Sources/Views/SearchBar.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift index 77fe27ff6..834081a7f 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift @@ -33,11 +33,13 @@ public struct EditLabelsSheet: View { UITextView.appearance().textContainerInset = UIEdgeInsets(top: 5, left: 2, bottom: 5, right: 2) } + @MainActor func onLabelTap(label: LinkedItemLabel, textChip _: TextChip) { - if labelsViewModel.selectedLabels.contains(label) { - labelsViewModel.selectedLabels.remove(label) + if let idx = labelsViewModel.selectedLabels.firstIndex(of: label) { + labelsViewModel.selectedLabels.remove(at: idx) } else { - labelsViewModel.selectedLabels.insert(label) + labelsViewModel.labelSearchFilter = "" + labelsViewModel.selectedLabels.append(label) } if let linkedItem = viewModel.linkedItem { @@ -47,13 +49,19 @@ public struct EditLabelsSheet: View { var content: some View { VStack(spacing: 15) { - SearchBar(searchTerm: $labelsViewModel.labelSearchFilter) + LabelsEntryView( + searchTerm: $labelsViewModel.labelSearchFilter, + viewModel: labelsViewModel + ) // swiftlint:disable line_length ScrollView { - LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter), - selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter), - onLabelTap: onLabelTap) + LabelsMasonaryView( + labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter), + selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter), + onLabelTap: onLabelTap + ) + Button( action: { labelsViewModel.showCreateLabelModal = true }, label: { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index ac264390e..b16b41ffe 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -1,3 +1,4 @@ + import Models import Services import SwiftUI @@ -49,9 +50,17 @@ struct ApplyLabelsView: View { var innerBody: some View { VStack { - SearchBar(searchTerm: $viewModel.labelSearchFilter) + if !viewModel.labels.isEmpty { + LabelsEntryView( + searchTerm: $viewModel.labelSearchFilter, + viewModel: viewModel + ) .padding(.vertical, 8) .padding(.horizontal, 16) + } + if viewModel.labelSearchFilter.count >= 63 { + Text("The maximum length of a label is 64 chars.").foregroundColor(Color.red).font(.footnote) + } List { Section { @@ -59,9 +68,12 @@ struct ApplyLabelsView: View { Button( action: { if isSelected(label) { - viewModel.selectedLabels.remove(label) + if let idx = viewModel.selectedLabels.firstIndex(of: label) { + viewModel.selectedLabels.remove(at: idx) + } } else { - viewModel.selectedLabels.insert(label) + viewModel.labelSearchFilter = "" + viewModel.selectedLabels.append(label) } }, label: { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index fe7c77b05..b39875853 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -2,13 +2,12 @@ import CoreData import Models import Services import SwiftUI -import Views @MainActor public final class LabelsViewModel: ObservableObject { let labelNameMaxLength = 64 @Published var isLoading = false - @Published var selectedLabels = Set() + @Published var selectedLabels = [LinkedItemLabel]() @Published var unselectedLabels = Set() @Published var labels = [LinkedItemLabel]() @Published var showCreateLabelModal = false @@ -36,7 +35,7 @@ import Views await loadLabelsFromStore(dataService: dataService) for label in labels { if selLabels.contains(label) { - selectedLabels.insert(label) + selectedLabels.append(label) } else { unselectedLabels.insert(label) } @@ -50,7 +49,7 @@ import Views } for label in self.labels { if selLabels.contains(label) { - self.selectedLabels.insert(label) + self.selectedLabels.append(label) } else { self.unselectedLabels.insert(label) } @@ -100,7 +99,7 @@ import Views if let label = dataService.viewContext.object(with: labelObjectID) as? LinkedItemLabel { labels.insert(label, at: 0) - selectedLabels.insert(label) + selectedLabels.append(label) } isLoading = false diff --git a/apple/OmnivoreKit/Sources/App/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/App/Views/SearchBar.swift new file mode 100644 index 000000000..6be364265 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/SearchBar.swift @@ -0,0 +1,200 @@ +import Models +import Services +import SwiftUI +import Views + +@MainActor +protocol Entry { + func item(parent: LabelsEntryView) -> AnyView +} + +@MainActor +private struct LabelEntry: Entry { + let label: LinkedItemLabel + + func item(parent _: LabelsEntryView) -> AnyView { + if let name = label.name, let hex = label.color, let color = Color(hex: hex) { + return AnyView(LibraryItemLabelView(text: name, color: color)) + } + return AnyView(EmptyView()) + } +} + +@MainActor +public struct LabelsEntryView: View { + @Binding var searchTerm: String + @State var viewModel: LabelsViewModel + @State var lastSelected = false + @State var justInserted = false + + let entries: [Entry] + + @State private var totalHeight = CGFloat.zero + @FocusState private var textFieldFocused: Bool + @FocusState private var neverFocused: Bool + + public init( + searchTerm: Binding, + viewModel: LabelsViewModel + ) { + self._searchTerm = searchTerm + self.viewModel = viewModel + + self.entries = Array(viewModel.selectedLabels.map { LabelEntry(label: $0) }) + } + + func onTextSubmit() { + if searchTerm.count < 1 { + return + } + + // first see if there is a matching label + let term = searchTerm.lowercased() + if let label = viewModel.labels.first(where: { $0.name?.lowercased() == term }) { + justInserted = true + searchTerm = "" + if !viewModel.selectedLabels.contains(label) { + viewModel.selectedLabels.append(label) + } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { + lastSelected = false + textFieldFocused = true + justInserted = false + } + } + } + + var deletableTextField: some View { + let str = NSAttributedString( + string: searchTerm, + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14)] + ) + // Round it up to avoid jitter when typing + let textWidth = max(25.0, Double(Int(str.size().width + 1))) + let result = TextField("", text: $searchTerm) + .frame(alignment: .topLeading) + .frame(height: 25) + .frame(width: textWidth) + .padding(5) + .font(Font.system(size: 14)) + .multilineTextAlignment(.leading) + .onChange(of: searchTerm, perform: { newValue in + print("NEW VALUE: ", newValue.count) + if searchTerm.count >= 64 { + searchTerm = String(searchTerm.prefix(64)) + } + if searchTerm.isEmpty { + // When we insert a new item we set the text to "" so this block is triggered + // we need to ignore that special case. + if justInserted { + justInserted = false + return + } + if lastSelected { + if viewModel.selectedLabels.count > 0 { + lastSelected = false + viewModel.selectedLabels.removeLast() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { + textFieldFocused = true + } + } + } else { + lastSelected = true + searchTerm = "\u{200B}" + } + } else if searchTerm != "\u{200B}" { + lastSelected = false + } + }) + .onSubmit { + onTextSubmit() + } + return result + } + + func onTextDelete() -> Bool { if searchTerm.isEmpty { + if lastSelected { + if viewModel.selectedLabels.count > 0 { + viewModel.selectedLabels.removeLast() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { + textFieldFocused = true + } + } + } else { + lastSelected = true + } + return true + } + return false + } + + public var body: some View { + // HStack(spacing: 0) { + VStack { + GeometryReader { geometry in + self.generateLabelsContent(in: geometry) + } + }.padding(0) + .frame(height: totalHeight) + .background(Color.extensionPanelBackground) + .cornerRadius(8) + .onAppear { + textFieldFocused = true + } + .onTapGesture { + textFieldFocused = true + } + .transaction { $0.animation = nil } + } + + private func generateLabelsContent(in geom: GeometryProxy) -> some View { + var width = CGFloat.zero + var height = CGFloat.zero + + return ZStack(alignment: .topLeading) { + ForEach(Array(self.entries.enumerated()), id: \.offset) { _, entry in + entry.item(parent: self) + .padding(5) + .alignmentGuide(.leading, computeValue: { dim in + if abs(width - dim.width) > geom.size.width { + width = 0 + height -= dim.height + } + let result = width + width -= dim.width + return result + }) + .alignmentGuide(.top, computeValue: { _ in + let result = height + return result + }) + } + + deletableTextField + .alignmentGuide(.leading, computeValue: { dim in + if abs(width - dim.width) > geom.size.width { + width = 0 + height -= dim.height + } + let result = width + width = 0 + return result + }) + .alignmentGuide(.top, computeValue: { _ in + let result = height + height = 0 + return result + }).focused($textFieldFocused) + }.background(viewHeightReader($totalHeight)) + } + + private func viewHeightReader(_ binding: Binding) -> some View { + GeometryReader { geometry -> Color in + let rect = geometry.frame(in: .local) + DispatchQueue.main.async { + binding.wrappedValue = rect.size.height + } + return .clear + } + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift index 76f11d506..0e47bffd6 100644 --- a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift +++ b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift @@ -50,6 +50,8 @@ public extension Color { static var extensionPanelBackground: Color { Color("_extensionPanelBackground", bundle: .module) } static var extensionTextSubtle: Color { Color("_extensionTextSubtle", bundle: .module) } + static var textFieldBackground: Color { Color("_textFieldBackground", bundle: .module) } + // Apple system UIColor equivalents #if os(iOS) static var systemBackground: Color { Color(.systemBackground) } diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_textFieldBackground.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_textFieldBackground.colorset/Contents.json new file mode 100644 index 000000000..127e33e80 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_textFieldBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2C", + "red" : "0x2C" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/Views/SearchBar.swift deleted file mode 100644 index ae9dc8957..000000000 --- a/apple/OmnivoreKit/Sources/Views/SearchBar.swift +++ /dev/null @@ -1,59 +0,0 @@ -import SwiftUI - -public struct SearchBar: View { - @Binding var searchTerm: String - @FocusState private var isFocused: Bool - - public init( - searchTerm: Binding - ) { - self._searchTerm = searchTerm - } - - public var body: some View { - HStack(spacing: 0) { - TextField("Add Labels", text: $searchTerm) - .frame(height: 36) - .frame(maxWidth: .infinity) - .padding(.leading, 28) - .padding(.trailing, 28) - .focused($isFocused) - .overlay( - HStack { - Image(systemName: "magnifyingglass") - .resizable() - .frame(width: 14, height: 14) - .foregroundColor(.appGrayText) - .padding(.leading, 8) - - Spacer() - } - ) - - if isFocused { - Button( - action: { - self.isFocused = false - }, - label: { - Image(systemName: "multiply.circle.fill") - .foregroundColor(.gray) - } - ) - .padding(.trailing, 8) - .transition(.move(edge: .trailing)) - } - } - .background(Color.appButtonBackground) - .cornerRadius(8) - .frame(height: 36) - .onChange(of: isFocused) { isFocused in - if !isFocused { - searchTerm = "" - } - } - .onTapGesture { - isFocused = true - } - } -} From 1b85fb0c15fade4fff0f755157f0f0972cf60cac Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Nov 2023 14:29:15 +0800 Subject: [PATCH 15/30] Add feeds, better delete handling --- .../Share/Views/EditLabelsSheet.swift | 2 +- .../App/Views/Home/HomeFeedDisplayText.swift | 2 + .../App/Views/Labels/ApplyLabelsView.swift | 8 +- .../App/Views/Labels/LabelsViewModel.swift | 10 ++- ...{SearchBar.swift => LabelsEntryView.swift} | 76 ++++++++----------- .../Sources/Models/LinkedItemFilter.swift | 8 ++ 6 files changed, 54 insertions(+), 52 deletions(-) rename apple/OmnivoreKit/Sources/App/Views/{SearchBar.swift => LabelsEntryView.swift} (72%) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift index 834081a7f..c9ca6c639 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/EditLabelsSheet.swift @@ -38,7 +38,7 @@ public struct EditLabelsSheet: View { if let idx = labelsViewModel.selectedLabels.firstIndex(of: label) { labelsViewModel.selectedLabels.remove(at: idx) } else { - labelsViewModel.labelSearchFilter = "" + labelsViewModel.labelSearchFilter = ZWSP labelsViewModel.selectedLabels.append(label) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift index eb57e0d4f..745d37a77 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift @@ -11,6 +11,8 @@ extension LinkedItemFilter { return LocalText.readLaterGeneric case .newsletters: return LocalText.newslettersGeneric + case .feeds: + return "Feeds" case .recommended: return "Recommended" case .all: diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index b16b41ffe..9b6129bd6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -72,7 +72,7 @@ struct ApplyLabelsView: View { viewModel.selectedLabels.remove(at: idx) } } else { - viewModel.labelSearchFilter = "" + viewModel.labelSearchFilter = ZWSP viewModel.selectedLabels.append(label) } }, @@ -204,9 +204,11 @@ struct ApplyLabelsView: View { extension Sequence where Element == LinkedItemLabel { func applySearchFilter(_ searchFilter: String) -> [LinkedItemLabel] { - if searchFilter.isEmpty { + if searchFilter.isEmpty || searchFilter == ZWSP { return map { $0 } // return the identity of the sequence } - return filter { ($0.name ?? "").lowercased().contains(searchFilter.lowercased()) } + let index = searchFilter.index(searchFilter.startIndex, offsetBy: 1) + let trimmed = searchFilter.suffix(from: index).lowercased() + return filter { ($0.name ?? "").lowercased().contains(trimmed) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index b39875853..11b652f16 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -11,7 +11,7 @@ import SwiftUI @Published var unselectedLabels = Set() @Published var labels = [LinkedItemLabel]() @Published var showCreateLabelModal = false - @Published var labelSearchFilter = "" + @Published var labelSearchFilter = ZWSP public init() {} @@ -35,7 +35,9 @@ import SwiftUI await loadLabelsFromStore(dataService: dataService) for label in labels { if selLabels.contains(label) { - selectedLabels.append(label) + if !selectedLabels.contains(label) { + selectedLabels.append(label) + } } else { unselectedLabels.insert(label) } @@ -49,7 +51,9 @@ import SwiftUI } for label in self.labels { if selLabels.contains(label) { - self.selectedLabels.append(label) + if !self.selectedLabels.contains(label) { + self.selectedLabels.append(label) + } } else { self.unselectedLabels.insert(label) } diff --git a/apple/OmnivoreKit/Sources/App/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift similarity index 72% rename from apple/OmnivoreKit/Sources/App/Views/SearchBar.swift rename to apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift index 6be364265..9cb8bab3f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/SearchBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift @@ -3,6 +3,8 @@ import Services import SwiftUI import Views +let ZWSP = "\u{200B}" + @MainActor protocol Entry { func item(parent: LabelsEntryView) -> AnyView @@ -24,14 +26,11 @@ private struct LabelEntry: Entry { public struct LabelsEntryView: View { @Binding var searchTerm: String @State var viewModel: LabelsViewModel - @State var lastSelected = false - @State var justInserted = false let entries: [Entry] @State private var totalHeight = CGFloat.zero @FocusState private var textFieldFocused: Bool - @FocusState private var neverFocused: Bool public init( searchTerm: Binding, @@ -44,22 +43,21 @@ public struct LabelsEntryView: View { } func onTextSubmit() { - if searchTerm.count < 1 { + let index = searchTerm.index(searchTerm.startIndex, offsetBy: 1) + let trimmed = searchTerm.suffix(from: index).lowercased() + + if trimmed.count < 1 { return } - // first see if there is a matching label - let term = searchTerm.lowercased() - if let label = viewModel.labels.first(where: { $0.name?.lowercased() == term }) { - justInserted = true - searchTerm = "" + if let label = viewModel.labels.first(where: { $0.name?.lowercased() == trimmed }) { if !viewModel.selectedLabels.contains(label) { viewModel.selectedLabels.append(label) } + + searchTerm = ZWSP DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - lastSelected = false textFieldFocused = true - justInserted = false } } } @@ -78,32 +76,20 @@ public struct LabelsEntryView: View { .padding(5) .font(Font.system(size: 14)) .multilineTextAlignment(.leading) - .onChange(of: searchTerm, perform: { newValue in - print("NEW VALUE: ", newValue.count) + .onChange(of: searchTerm, perform: { _ in if searchTerm.count >= 64 { searchTerm = String(searchTerm.prefix(64)) } if searchTerm.isEmpty { - // When we insert a new item we set the text to "" so this block is triggered - // we need to ignore that special case. - if justInserted { - justInserted = false - return - } - if lastSelected { - if viewModel.selectedLabels.count > 0 { - lastSelected = false - viewModel.selectedLabels.removeLast() - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - textFieldFocused = true - } + if viewModel.selectedLabels.count > 0 { + viewModel.selectedLabels.removeLast() + searchTerm = ZWSP + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { + textFieldFocused = true } } else { - lastSelected = true - searchTerm = "\u{200B}" + searchTerm = ZWSP } - } else if searchTerm != "\u{200B}" { - lastSelected = false } }) .onSubmit { @@ -112,21 +98,21 @@ public struct LabelsEntryView: View { return result } - func onTextDelete() -> Bool { if searchTerm.isEmpty { - if lastSelected { - if viewModel.selectedLabels.count > 0 { - viewModel.selectedLabels.removeLast() - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { - textFieldFocused = true - } - } - } else { - lastSelected = true - } - return true - } - return false - } +// func onTextDelete() -> Bool { if searchTerm.isEmpty { +// if lastSelected { +// if viewModel.selectedLabels.count > 0 { +// viewModel.selectedLabels.removeLast() +// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { +// textFieldFocused = true +// } +// } +// } else { +// lastSelected = true +// } +// return true +// } +// return false +// } public var body: some View { // HStack(spacing: 0) { diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift index b9e819a32..be54b26ef 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -2,6 +2,7 @@ import Foundation public enum LinkedItemFilter: String, CaseIterable { case inbox + case feeds case readlater case newsletters case recommended @@ -17,6 +18,8 @@ public extension LinkedItemFilter { switch self { case .inbox: return "in:inbox" + case .feeds: + return "label:RSS" case .readlater: return "in:library" case .newsletters: @@ -76,6 +79,11 @@ public extension LinkedItemFilter { format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0" ) return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, newsletterLabelPredicate]) + case .feeds: + let feedLabelPredicate = NSPredicate( + format: "SUBQUERY(labels, $label, $label.name == \"RSS\").@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, feedLabelPredicate]) case .recommended: // non-archived or deleted items with the Newsletter label let recommendedPredicate = NSPredicate( From 557ca6adaa925175e9b676e8637482bd3a5abd77 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Nov 2023 14:39:43 +0800 Subject: [PATCH 16/30] Create labels on submit --- .../Sources/App/Views/LabelsEntryView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift b/apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift index 9cb8bab3f..7a5141799 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift @@ -26,6 +26,7 @@ private struct LabelEntry: Entry { public struct LabelsEntryView: View { @Binding var searchTerm: String @State var viewModel: LabelsViewModel + @EnvironmentObject var dataService: DataService let entries: [Entry] @@ -55,6 +56,17 @@ public struct LabelsEntryView: View { viewModel.selectedLabels.append(label) } + searchTerm = ZWSP + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { + textFieldFocused = true + } + } else { + viewModel.createLabel( + dataService: dataService, + name: trimmed, + color: Gradient.randomColor(str: trimmed, offset: 1), + description: nil + ) searchTerm = ZWSP DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { textFieldFocused = true From 3575008e28291a88344a1f91b7754ecf8ca93e6f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Nov 2023 15:15:55 +0800 Subject: [PATCH 17/30] Set iconURL from response if possible --- .../AppExtensions/Share/ShareExtensionViewModel.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index 37792c953..e74d34a55 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -192,6 +192,16 @@ public class ShareExtensionViewModel: ObservableObject { if let title = self.linkedItem?.title { self.title = title } + if let iconURL = self.linkedItem?.imageURL { + self.iconURL = iconURL + } + if let noteHighlight = self.linkedItem?.highlights? + .compactMap({ $0 as? Highlight }) + .first(where: { $0.type == "NOTE" }), + let noteText = noteHighlight.annotation + { + self.noteText = noteText + } if let urlStr = self.linkedItem?.pageURLString, let hostname = URL(string: urlStr)?.host { self.url = hostname } else { From a28f618b08df82543f6162f436b0e6b4e22fff35 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Nov 2023 15:16:05 +0800 Subject: [PATCH 18/30] Consistent casing for titles --- packages/web/components/patterns/CardMenu.tsx | 12 ++++++------ packages/web/components/patterns/HighlightBar.tsx | 6 +++--- .../patterns/LibraryCards/LibraryHoverActions.tsx | 2 +- .../web/components/patterns/ReaderDropdownMenu.tsx | 4 ++-- packages/web/components/templates/UploadModal.tsx | 2 +- .../templates/article/AddBulkLabelsModal.tsx | 2 +- .../templates/article/ArticleActionsMenu.tsx | 4 ++-- .../components/templates/article/NotebookModal.tsx | 4 ++-- .../templates/article/VerticalArticleActions.tsx | 6 +++--- .../templates/homeFeed/LibraryFilterMenu.tsx | 2 +- .../web/components/templates/reader/ReaderHeader.tsx | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/web/components/patterns/CardMenu.tsx b/packages/web/components/patterns/CardMenu.tsx index 27b1207b4..3f190f15c 100644 --- a/packages/web/components/patterns/CardMenu.tsx +++ b/packages/web/components/patterns/CardMenu.tsx @@ -44,35 +44,35 @@ export function CardMenu(props: CardMenuProps): JSX.Element { onSelect={() => { props.actionHandler('set-labels') }} - title="Set Labels" + title="Set labels" /> { props.actionHandler('open-notebook') }} - title="Open Notebook" + title="Open notebook" /> props.actionHandler('showOriginal')} - title="Open Original" + title="Open original" /> props.actionHandler('editTitle')} - title="Edit Metadata" + title="Edit metadata" /> {props.item.readingProgressPercent < 98 ? ( { props.actionHandler('mark-read') }} - title="Mark Read" + title="Mark read" /> ) : ( { props.actionHandler('mark-unread') }} - title="Mark Unread" + title="Mark unread" /> )} { if (!props.isNewHighlight && props.highlightColor != color) { props.handleButtonClick('updateColor', color) @@ -151,7 +151,7 @@ function BarContent(props: HighlightBarProps): JSX.Element { {!props.isNewHighlight && ( <> )}