From 584aac6a3b06d54305736345b5ea87579931f53d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 10 May 2022 16:24:06 -0700 Subject: [PATCH 01/11] replace searchable with a custom search bar --- .../App/Views/Home/HomeFeedViewIOS.swift | 26 ++++++------ .../OmnivoreKit/Sources/Views/SearchBar.swift | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Views/SearchBar.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 62bd31f04..c70f0c646 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -26,16 +26,16 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone .refreshable { loadItems(isRefresh: true) } - .searchable( - text: $viewModel.searchTerm - ) { - if viewModel.searchTerm.isEmpty { - Text("Inbox").searchCompletion("in:inbox ") - Text("All").searchCompletion("in:all ") - Text("Archived").searchCompletion("in:archive ") - Text("Files").searchCompletion("type:file ") - } - } +// .searchable( +// text: $viewModel.searchTerm +// ) { +// if viewModel.searchTerm.isEmpty { +// Text("Inbox").searchCompletion("in:inbox ") +// Text("All").searchCompletion("in:all ") +// Text("Archived").searchCompletion("in:archive ") +// Text("Files").searchCompletion("type:file ") +// } +// } .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without @@ -44,9 +44,6 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone .onChange(of: viewModel.selectedLabels) { _ in loadItems(isRefresh: true) } - .onSubmit(of: .search) { - loadItems(isRefresh: true) - } .sheet(item: $viewModel.itemUnderLabelEdit) { item in ApplyLabelsView(mode: .item(item), onSave: nil) } @@ -131,10 +128,13 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool @State private var showLabelsSheet = false + @State private var isSearching = false @ObservedObject var viewModel: HomeFeedViewModel var body: some View { VStack(spacing: 0) { + SearchBar(searchTerm: $viewModel.searchTerm, isSearching: $isSearching) + .padding(.bottom) ZStack(alignment: .bottom) { ScrollView(.horizontal, showsIndicators: false) { HStack { diff --git a/apple/OmnivoreKit/Sources/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/Views/SearchBar.swift new file mode 100644 index 000000000..596796480 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/SearchBar.swift @@ -0,0 +1,40 @@ +import SwiftUI + +public struct SearchBar: View { + @Binding var searchTerm: String + @Binding var isSearching: Bool + + public init( + searchTerm: Binding, + isSearching: Binding + ) { + self._searchTerm = searchTerm + self._isSearching = isSearching + } + + public var body: some View { + HStack(spacing: 0) { + TextField("Search", text: $searchTerm) + .padding(.vertical, 8) + .padding(.horizontal, 8) + .background(Color(.systemGray6)) + .cornerRadius(8) + .padding(.horizontal, 10) + .onTapGesture { + self.isSearching = true + } + + if isSearching { + Button( + action: { + self.isSearching = false + self.searchTerm = "" + }, + label: { Text("Cancel") } + ) + .padding(.trailing) + .transition(.move(edge: .trailing)) + } + } + } +} From fb77ceddf2bea9a8f17c52c831247cf44a44268c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 10 May 2022 17:15:42 -0700 Subject: [PATCH 02/11] use menu to display item filters --- .../App/Views/Home/HomeFeedViewIOS.swift | 21 +++--- .../App/Views/Home/HomeFeedViewModel.swift | 58 +++-------------- .../Sources/Models/LinkedItemFilter.swift | 65 +++++++++++++++++++ .../OmnivoreKit/Sources/Views/TextChip.swift | 6 +- 4 files changed, 89 insertions(+), 61 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index c70f0c646..69b5028c7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -26,16 +26,6 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone .refreshable { loadItems(isRefresh: true) } -// .searchable( -// text: $viewModel.searchTerm -// ) { -// if viewModel.searchTerm.isEmpty { -// Text("Inbox").searchCompletion("in:inbox ") -// Text("All").searchCompletion("in:all ") -// Text("Archived").searchCompletion("in:archive ") -// Text("Files").searchCompletion("type:file ") -// } -// } .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without @@ -44,6 +34,9 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone .onChange(of: viewModel.selectedLabels) { _ in loadItems(isRefresh: true) } + .onChange(of: viewModel.appliedFilter) { _ in + loadItems(isRefresh: true) + } .sheet(item: $viewModel.itemUnderLabelEdit) { item in ApplyLabelsView(mode: .item(item), onSave: nil) } @@ -138,6 +131,14 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone ZStack(alignment: .bottom) { ScrollView(.horizontal, showsIndicators: false) { HStack { + Menu( + content: { + ForEach(LinkedItemFilter.allCases, id: \.self) { filter in + Button(filter.displayName, action: { viewModel.appliedFilter = filter }) + } + }, + label: { TextChipButton.makeFilterButton(title: viewModel.appliedFilter.displayName) } + ) TextChipButton.makeAddLabelButton { showLabelsSheet = true } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 1d46f3235..82d595cdb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -18,6 +18,7 @@ import Views @Published var itemToSnoozeID: String? @Published var selectedLinkItem: LinkedItem? @Published var showLoadingBar = false + @Published var appliedFilter = LinkedItemFilter.all var cursor: String? @@ -84,11 +85,11 @@ import Views cursor = queryResult.cursor await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID)) showLoadingBar = false - } else if searchTermIsEmpty { + } else if searchTerm.replacingOccurrences(of: " ", with: "").isEmpty { await dataService.viewContext.perform { let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false)] - fetchRequest.predicate = self.itemRequestPredicate + fetchRequest.predicate = self.appliedFilter.predicate // // TODO: Filter on label if let fetchedItems = try? dataService.viewContext.fetch(fetchRequest) { @@ -101,49 +102,6 @@ import Views } } - private var itemRequestPredicate: NSPredicate { - let undeletedPredicate = NSPredicate( - format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) - ) - - if searchTerm.contains("in:all") { - // include everything undeleted - return undeletedPredicate - } - - if searchTerm.contains("in:archive") { - let inArchivePredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: true) as NSNumber - ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate]) - } - - if searchTerm.contains("type:file") { - // include pdf only - let isPDFPredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.contentReader), "PDF" - ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate]) - } - - // default to "in:inbox" (non-archived items) - let notInArchivePredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber - ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate]) - } - - // Exclude filters when testing if user has enetered a search term - private var searchTermIsEmpty: Bool { - searchTerm - .replacingOccurrences(of: "in:inbox", with: "") - .replacingOccurrences(of: "in:all", with: "") - .replacingOccurrences(of: "in:archive", with: "") - .replacingOccurrences(of: "type:file", with: "") - .replacingOccurrences(of: " ", with: "") - .isEmpty - } - func setLinkArchived(dataService: DataService, objectID: NSManagedObjectID, archived: Bool) { // TODO: remove this by making list always fetch from Coredata guard let itemIndex = items.firstIndex(where: { $0.objectID == objectID }) else { return } @@ -182,12 +140,12 @@ import Views isLoading = false } - private var searchQuery: String? { - if searchTerm.isEmpty, selectedLabels.isEmpty { - return nil - } + private var searchQuery: String { + var query = "\(appliedFilter.queryString)" - var query = searchTerm + if !searchTerm.isEmpty { + query.append(" \(searchTerm)") + } if !selectedLabels.isEmpty { query.append(" label:") diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift new file mode 100644 index 000000000..cc58226ce --- /dev/null +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -0,0 +1,65 @@ +import Foundation + +public enum LinkedItemFilter: CaseIterable { + case inbox + case all + case archived + case files +} + +public extension LinkedItemFilter { + var displayName: String { + switch self { + case .inbox: + return "Inbox" + case .all: + return "All" + case .archived: + return "Archived" + case .files: + return "Files" + } + } + + var queryString: String { + switch self { + case .inbox: + return "in:inbox" + case .all: + return "in:all" + case .archived: + return "in:archive" + case .files: + return "type:file" + } + } + + var predicate: NSPredicate { + let undeletedPredicate = NSPredicate( + format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) + ) + + switch self { + case .inbox: + // non-archived items + let notInArchivePredicate = NSPredicate( + format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate]) + case .all: + // include everything undeleted + return undeletedPredicate + case .archived: + let inArchivePredicate = NSPredicate( + format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: true) as NSNumber + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate]) + case .files: + // include pdf only + let isPDFPredicate = NSPredicate( + format: "%K == %@", #keyPath(LinkedItem.contentReader), "PDF" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate]) + } + } +} diff --git a/apple/OmnivoreKit/Sources/Views/TextChip.swift b/apple/OmnivoreKit/Sources/Views/TextChip.swift index 4e0ada9c4..20a6a4d13 100644 --- a/apple/OmnivoreKit/Sources/Views/TextChip.swift +++ b/apple/OmnivoreKit/Sources/Views/TextChip.swift @@ -34,6 +34,10 @@ public struct TextChipButton: View { TextChipButton(title: "Labels", color: .systemGray6, actionType: .show, onTap: onTap) } + public static func makeFilterButton(title: String) -> TextChipButton { + TextChipButton(title: title, color: .systemGray6, actionType: .show, onTap: {}) + } + public static func makeShowOptionsButton(title: String, onTap: @escaping () -> Void) -> TextChipButton { TextChipButton(title: title, color: .appButtonBackground, actionType: .add, onTap: onTap) } @@ -67,7 +71,7 @@ public struct TextChipButton: View { } } - init(title: String, color: Color, actionType: ActionType, onTap: @escaping () -> Void) { + public init(title: String, color: Color, actionType: ActionType, onTap: @escaping () -> Void) { self.text = title self.color = color self.onTap = onTap From 4b1d772f00f7efb96848f3cca732c3bff2c108ae Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 10 May 2022 20:41:27 -0700 Subject: [PATCH 03/11] use xmark to clear search bar --- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 2 +- apple/OmnivoreKit/Sources/Views/SearchBar.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 82d595cdb..3c2a2eb5c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -18,7 +18,7 @@ import Views @Published var itemToSnoozeID: String? @Published var selectedLinkItem: LinkedItem? @Published var showLoadingBar = false - @Published var appliedFilter = LinkedItemFilter.all + @Published var appliedFilter = LinkedItemFilter.inbox var cursor: String? diff --git a/apple/OmnivoreKit/Sources/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/Views/SearchBar.swift index 596796480..ad60c2734 100644 --- a/apple/OmnivoreKit/Sources/Views/SearchBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SearchBar.swift @@ -30,10 +30,10 @@ public struct SearchBar: View { self.isSearching = false self.searchTerm = "" }, - label: { Text("Cancel") } + label: { Image(systemName: "xmark.circle").foregroundColor(.appGrayTextContrast) } ) .padding(.trailing) - .transition(.move(edge: .trailing)) + .transition(.opacity) } } } From d041c73fd12a1a01fb20e2067de6ce3ab606bd02 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 10 May 2022 21:16:45 -0700 Subject: [PATCH 04/11] show nav bar on tap in the web reader --- .../Sources/App/Views/WebReader/WebReader.swift | 6 ++++++ .../Sources/App/Views/WebReader/WebReaderContainer.swift | 8 ++++++++ .../App/Views/WebReader/WebReaderCoordinator.swift | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index 7e219b424..c1001ca07 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -16,6 +16,7 @@ import WebKit @Binding var increaseFontActionID: UUID? @Binding var decreaseFontActionID: UUID? @Binding var annotationSaveTransactionID: UUID? + @Binding var showNavBarActionID: UUID? @Binding var annotation: String func makeCoordinator() -> WebReaderCoordinator { @@ -75,6 +76,11 @@ import WebKit (webView as? WebView)?.decreaseFontSize() } + if showNavBarActionID != context.coordinator.previousShowNavBarActionID { + context.coordinator.previousShowNavBarActionID = showNavBarActionID + context.coordinator.showNavBar() + } + // If the webview had been terminated `needsReload` will have been set to true if context.coordinator.needsReload { loadContent(webView: webView) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 2a102607a..6126708f2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -18,6 +18,7 @@ import WebKit @State var increaseFontActionID: UUID? @State var decreaseFontActionID: UUID? @State var annotationSaveTransactionID: UUID? + @State var showNavBarActionID: UUID? @State var annotation = String() @EnvironmentObject var dataService: DataService @@ -154,8 +155,15 @@ import WebKit increaseFontActionID: $increaseFontActionID, decreaseFontActionID: $decreaseFontActionID, annotationSaveTransactionID: $annotationSaveTransactionID, + showNavBarActionID: $showNavBarActionID, annotation: $annotation ) + .onTapGesture { + withAnimation { + navBarVisibilityRatio = 1 + showNavBarActionID = UUID() + } + } .sheet(item: $safariWebLink) { SafariView(url: $0.url) } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift index 30743cb71..7ed3a19f6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift @@ -17,6 +17,7 @@ final class WebReaderCoordinator: NSObject { var lastSavedAnnotationID: UUID? var previousIncreaseFontActionID: UUID? var previousDecreaseFontActionID: UUID? + var previousShowNavBarActionID: UUID? var updateNavBarVisibilityRatio: (Double) -> Void = { _ in } private var yOffsetAtStartOfDrag: Double? private var lastYOffset: Double = 0 @@ -33,6 +34,10 @@ final class WebReaderCoordinator: NSObject { updateNavBarVisibilityRatio(navBarVisibilityRatio) } } + + func showNavBar() { + isNavBarHidden = false + } } extension WebReaderCoordinator: WKScriptMessageHandler { From 3410a58a891726ee424587bef481ed0d30bc74d2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 May 2022 13:09:30 -0700 Subject: [PATCH 05/11] adjust tappable area of text chip buttons --- .../App/Views/Home/HomeFeedViewIOS.swift | 2 +- .../OmnivoreKit/Sources/Views/TextChip.swift | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 69b5028c7..7fff92964 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -127,7 +127,7 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone var body: some View { VStack(spacing: 0) { SearchBar(searchTerm: $viewModel.searchTerm, isSearching: $isSearching) - .padding(.bottom) + ZStack(alignment: .bottom) { ScrollView(.horizontal, showsIndicators: false) { HStack { diff --git a/apple/OmnivoreKit/Sources/Views/TextChip.swift b/apple/OmnivoreKit/Sources/Views/TextChip.swift index 20a6a4d13..2c2bd5c8b 100644 --- a/apple/OmnivoreKit/Sources/Views/TextChip.swift +++ b/apple/OmnivoreKit/Sources/Views/TextChip.swift @@ -91,23 +91,21 @@ public struct TextChipButton: View { let foregroundColor: Color public var body: some View { - Button(action: onTap) { - VStack(spacing: 0) { - HStack { - Text(text) - .padding(.leading, 3) - Image(systemName: actionType.systemIconName) - } - .padding(.horizontal, 10) - .padding(.vertical, 8) - .font(.appFootnote) - .foregroundColor(foregroundColor) - .lineLimit(1) - .background(Capsule().fill(color)) - - Color.clear.contentShape(Rectangle()).frame(height: 15) + VStack(spacing: 0) { + HStack { + Text(text) + .padding(.leading, 3) + Image(systemName: actionType.systemIconName) } - .contentShape(Rectangle()) + .padding(.horizontal, 10) + .padding(.vertical, 8) + .font(.appFootnote) + .foregroundColor(foregroundColor) + .lineLimit(1) + .background(Capsule().fill(color)) } + .padding(.vertical, 12) + .contentShape(Rectangle()) + .onTapGesture { onTap() } } } From f3aac3e30c8949a5af46ea74a02c7fa2e5c6606b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 May 2022 13:21:05 -0700 Subject: [PATCH 06/11] persist last used filter to user defaults --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 10 +++++++--- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 11 ++++++++--- .../OmnivoreKit/Sources/Models/LinkedItemFilter.swift | 2 +- apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift | 1 + 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 7fff92964..aa98ab456 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -127,17 +127,21 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone var body: some View { VStack(spacing: 0) { SearchBar(searchTerm: $viewModel.searchTerm, isSearching: $isSearching) - + ZStack(alignment: .bottom) { ScrollView(.horizontal, showsIndicators: false) { HStack { Menu( content: { ForEach(LinkedItemFilter.allCases, id: \.self) { filter in - Button(filter.displayName, action: { viewModel.appliedFilter = filter }) + Button(filter.displayName, action: { viewModel.appliedFilter = filter.rawValue }) } }, - label: { TextChipButton.makeFilterButton(title: viewModel.appliedFilter.displayName) } + label: { + TextChipButton.makeFilterButton( + title: LinkedItemFilter(rawValue: viewModel.appliedFilter)?.displayName ?? "Filter" + ) + } ) TextChipButton.makeAddLabelButton { showLabelsSheet = true diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 3c2a2eb5c..d16de27b1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -18,7 +18,9 @@ import Views @Published var itemToSnoozeID: String? @Published var selectedLinkItem: LinkedItem? @Published var showLoadingBar = false - @Published var appliedFilter = LinkedItemFilter.inbox + + @AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) + var appliedFilter = LinkedItemFilter.inbox.rawValue var cursor: String? @@ -89,7 +91,9 @@ import Views await dataService.viewContext.perform { let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false)] - fetchRequest.predicate = self.appliedFilter.predicate + if let predicate = LinkedItemFilter(rawValue: self.appliedFilter)?.predicate { + fetchRequest.predicate = predicate + } // // TODO: Filter on label if let fetchedItems = try? dataService.viewContext.fetch(fetchRequest) { @@ -141,7 +145,8 @@ import Views } private var searchQuery: String { - var query = "\(appliedFilter.queryString)" + let filter = LinkedItemFilter(rawValue: appliedFilter) ?? .inbox + var query = "\(filter.queryString)" if !searchTerm.isEmpty { query.append(" \(searchTerm)") diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift index cc58226ce..1b931c203 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -1,6 +1,6 @@ import Foundation -public enum LinkedItemFilter: CaseIterable { +public enum LinkedItemFilter: String, CaseIterable { case inbox case all case archived diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index 369dc232c..faa4d1e65 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -5,4 +5,5 @@ public enum UserDefaultKey: String { case userHasDeniedPushPrimer case firebasePushToken case homeFeedlayoutPreference + case lastSelectedLinkedItemFilter } From b45120706bd7bd778ad573cb3b80ef2879dda1fc Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 May 2022 16:23:22 -0700 Subject: [PATCH 07/11] fix linkeditem and labels data relationships. reset coredata on new app installs --- .../App/Views/Labels/LabelsViewModel.swift | 2 +- .../CoreDataModel.xcdatamodel/contents | 5 +- .../Services/DataService/DataService.swift | 49 +++++++++++++++++-- .../Mutations/RemoveLabelPublisher.swift | 6 +-- .../UpdateArticleLabelsPublisher.swift | 9 ++-- .../InternalLinkedItemLabel.swift | 8 +-- .../Sources/Utils/UserDefaultKeys.swift | 1 + 7 files changed, 59 insertions(+), 21 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index e0a7aa062..06b8ceefb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -64,7 +64,7 @@ import Views } func saveItemLabelChanges(itemID: String, dataService: DataService) { - dataService.updateItemLabels(itemID: itemID, labelNames: selectedLabels.map(\.unwrappedName)) + dataService.updateItemLabels(itemID: itemID, labelIDs: selectedLabels.map(\.unwrappedID)) } func addLabelToItem(_ label: LinkedItemLabel) { diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index 8b34e1252..183be56df 100644 --- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -41,7 +41,7 @@ - + @@ -55,6 +55,7 @@ + @@ -85,7 +86,7 @@ - + diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 145e0dade..3528a3dab 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -3,6 +3,7 @@ import CoreData import Foundation import Models import OSLog +import Utils let logger = Logger(subsystem: "app.omnivore", category: "data-service") @@ -13,8 +14,8 @@ public final class DataService: ObservableObject { public let appEnvironment: AppEnvironment let networker: Networker - let persistentContainer: PersistentContainer - let backgroundContext: NSManagedObjectContext + var persistentContainer: PersistentContainer + var backgroundContext: NSManagedObjectContext var subscriptions = Set() public var viewContext: NSManagedObjectContext { @@ -28,9 +29,13 @@ public final class DataService: ObservableObject { self.backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - persistentContainer.loadPersistentStores { _, error in - if let error = error { - fatalError("Core Data store failed to load with error: \(error)") + if isFirstTimeRunningNewAppVersion() { + resetCoreData() + } else { + persistentContainer.loadPersistentStores { _, error in + if let error = error { + fatalError("Core Data store failed to load with error: \(error)") + } } } } @@ -49,4 +54,38 @@ public final class DataService: ObservableObject { fatalError("Unable to write to Keychain: \(error)") } } + + private func resetCoreData() { + let storeContainer = + persistentContainer.persistentStoreCoordinator + + do { + for store in storeContainer.persistentStores { + try storeContainer.destroyPersistentStore( + at: store.url!, + ofType: store.type, + options: nil + ) + } + persistentContainer = PersistentContainer.make() + persistentContainer.loadPersistentStores { _, error in + if let error = error { + fatalError("Core Data store failed to load with error: \(error)") + } + } + backgroundContext = persistentContainer.newBackgroundContext() + } catch { + logger.debug("Failed to reset core data stores") + } + } + + private func isFirstTimeRunningNewAppVersion() -> Bool { + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") + guard let appVersion = appVersion as? String else { return false } + + let lastUsedAppVersion = UserDefaults.standard.string(forKey: UserDefaultKey.lastUsedAppVersion.rawValue) + let isFirstRun = (lastUsedAppVersion ?? "unknown") != appVersion + UserDefaults.standard.set(appVersion, forKey: UserDefaultKey.lastUsedAppVersion.rawValue) + return isFirstRun + } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift index 30e6844ce..fc7c02c2b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift @@ -7,7 +7,7 @@ extension DataService { // Update CoreData backgroundContext.perform { [weak self] in guard let self = self else { return } - guard let label = LinkedItemLabel.lookup(byName: name, inContext: self.backgroundContext) else { return } + guard let label = LinkedItemLabel.lookup(byID: labelID, inContext: self.backgroundContext) else { return } label.remove(inContext: self.backgroundContext) // Send update to server @@ -15,7 +15,7 @@ extension DataService { } } - func syncLabelDeletion(labelID: String, labelName: String) { + func syncLabelDeletion(labelID: String, labelName _: String) { enum MutationResult { case success(labelID: String) case error(errorCode: Enums.DeleteLabelErrorCode) @@ -43,7 +43,7 @@ extension DataService { let isSyncSuccess = data != nil context.perform { - let label = LinkedItemLabel.lookup(byName: labelName, inContext: context) + let label = LinkedItemLabel.lookup(byID: labelID, inContext: context) guard let label = label else { return } if isSyncSuccess { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift index 9b4fa549d..cab778898 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift @@ -4,7 +4,7 @@ import Models import SwiftGraphQL extension DataService { - public func updateItemLabels(itemID: String, labelNames: [String]) { + public func updateItemLabels(itemID: String, labelIDs: [String]) { backgroundContext.perform { [weak self] in guard let self = self else { return } guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } @@ -13,12 +13,9 @@ extension DataService { linkedItem.removeFromLabels(existingLabels) } - var labelIDs = [String]() - - for labelName in labelNames { - if let labelObject = LinkedItemLabel.lookup(byName: labelName, inContext: self.backgroundContext) { + for labelID in labelIDs { + if let labelObject = LinkedItemLabel.lookup(byID: labelID, inContext: self.backgroundContext) { linkedItem.addToLabels(labelObject) - labelIDs.append(labelObject.unwrappedID) } } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift index ca31ae33e..f54cf6f8b 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift @@ -29,8 +29,8 @@ struct InternalLinkedItemLabel { } func asManagedObject(inContext context: NSManagedObjectContext) -> LinkedItemLabel { - let existingItem = LinkedItemLabel.lookup(byName: name, inContext: context) - let label = existingItem ?? LinkedItemLabel(entity: LinkedItemLabel.entity(), insertInto: context) + let existingLabel = LinkedItemLabel.lookup(byID: id, inContext: context) + let label = existingLabel ?? LinkedItemLabel(entity: LinkedItemLabel.entity(), insertInto: context) label.id = id label.name = name label.color = color @@ -44,10 +44,10 @@ extension LinkedItemLabel { public var unwrappedID: String { id ?? "" } public var unwrappedName: String { name ?? "" } - static func lookup(byName name: String, inContext context: NSManagedObjectContext) -> LinkedItemLabel? { + static func lookup(byID id: String, inContext context: NSManagedObjectContext) -> LinkedItemLabel? { let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() fetchRequest.predicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItemLabel.name), name + format: "id == %@", id ) var label: LinkedItemLabel? diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index faa4d1e65..c32f00401 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -6,4 +6,5 @@ public enum UserDefaultKey: String { case firebasePushToken case homeFeedlayoutPreference case lastSelectedLinkedItemFilter + case lastUsedAppVersion } From 07c6792cde8d9a84bf031c67c4adcad361d86ce1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 May 2022 16:33:04 -0700 Subject: [PATCH 08/11] Add Newsletters and Read Later shortcuts --- .../Sources/Models/LinkedItemFilter.swift | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift index 1b931c203..c257b92ef 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -2,6 +2,8 @@ import Foundation public enum LinkedItemFilter: String, CaseIterable { case inbox + case readlater + case newsletters case all case archived case files @@ -12,6 +14,10 @@ public extension LinkedItemFilter { switch self { case .inbox: return "Inbox" + case .readlater: + return "Read Later" + case .newsletters: + return "Newsletters" case .all: return "All" case .archived: @@ -25,6 +31,10 @@ public extension LinkedItemFilter { switch self { case .inbox: return "in:inbox" + case .readlater: + return "in:inbox -label:Newsletter" + case .newsletters: + return "in:inbox label:Newsletter" case .all: return "in:all" case .archived: @@ -38,14 +48,22 @@ public extension LinkedItemFilter { let undeletedPredicate = NSPredicate( format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) ) + let notInArchivePredicate = NSPredicate( + format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber + ) switch self { case .inbox: // non-archived items - let notInArchivePredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber - ) return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate]) + case .readlater: + // non-archived or deleted items without the Newsletter label + let nonNewsletterLabelPredicate = NSPredicate(format: "NOT SUBQUERY(labels, $label, $label.name == \"Newsletter\") .@count > 0") + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, nonNewsletterLabelPredicate]) + case .newsletters: + // non-archived or deleted items with the Newsletter label + let newsletterLabelPredicate = NSPredicate(format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0") + return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, newsletterLabelPredicate]) case .all: // include everything undeleted return undeletedPredicate From 7f8f6833c51d88023725d4706af78f225d04a133 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 May 2022 17:14:28 -0700 Subject: [PATCH 09/11] hide keyboard when cancelling on search bar --- apple/OmnivoreKit/Sources/Views/SearchBar.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apple/OmnivoreKit/Sources/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/Views/SearchBar.swift index ad60c2734..e7edd548d 100644 --- a/apple/OmnivoreKit/Sources/Views/SearchBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SearchBar.swift @@ -29,6 +29,7 @@ public struct SearchBar: View { action: { self.isSearching = false self.searchTerm = "" + self.hideKeyboard() }, label: { Image(systemName: "xmark.circle").foregroundColor(.appGrayTextContrast) } ) From ad567a380eaefe33eb92b331bcb2982eb9bda883 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 May 2022 17:47:03 -0700 Subject: [PATCH 10/11] Add search overlays for cancel and the mag. glass --- .../App/Views/Home/HomeFeedViewIOS.swift | 3 +- .../OmnivoreKit/Sources/Views/SearchBar.swift | 53 ++++++++++++------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index aa98ab456..4cfe091dd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -121,12 +121,11 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool @State private var showLabelsSheet = false - @State private var isSearching = false @ObservedObject var viewModel: HomeFeedViewModel var body: some View { VStack(spacing: 0) { - SearchBar(searchTerm: $viewModel.searchTerm, isSearching: $isSearching) + SearchBar(searchTerm: $viewModel.searchTerm) ZStack(alignment: .bottom) { ScrollView(.horizontal, showsIndicators: false) { diff --git a/apple/OmnivoreKit/Sources/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/Views/SearchBar.swift index e7edd548d..81edaa3c6 100644 --- a/apple/OmnivoreKit/Sources/Views/SearchBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SearchBar.swift @@ -2,39 +2,52 @@ import SwiftUI public struct SearchBar: View { @Binding var searchTerm: String - @Binding var isSearching: Bool + @FocusState private var isFocused: Bool public init( - searchTerm: Binding, - isSearching: Binding + searchTerm: Binding ) { self._searchTerm = searchTerm - self._isSearching = isSearching } public var body: some View { HStack(spacing: 0) { TextField("Search", text: $searchTerm) - .padding(.vertical, 8) - .padding(.horizontal, 8) + .padding(7) + .padding(.horizontal, 25) .background(Color(.systemGray6)) .cornerRadius(8) - .padding(.horizontal, 10) - .onTapGesture { - self.isSearching = true - } + .focused($isFocused) + .overlay( + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .padding(.leading, 10) - if isSearching { - Button( - action: { - self.isSearching = false - self.searchTerm = "" - self.hideKeyboard() - }, - label: { Image(systemName: "xmark.circle").foregroundColor(.appGrayTextContrast) } + if self.searchTerm != "" { + Button(action: { + self.searchTerm = "" + self.isFocused = false + }) { + Image(systemName: "multiply.circle.fill") + .foregroundColor(.gray) + .padding(.trailing, 8) + } + } + } ) - .padding(.trailing) - .transition(.opacity) + .padding(.horizontal, 10) + + if isFocused { + Button(action: { + self.isFocused = false + self.searchTerm = "" + }) { + Text("Cancel") + } + .padding(.trailing, 10) + .transition(.move(edge: .trailing)) } } } From a199d8b3bc4577f7e4094a2f2be5930a4b02a8df Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 May 2022 18:24:40 -0700 Subject: [PATCH 11/11] keep search field focus when clearing search text --- .../OmnivoreKit/Sources/Views/SearchBar.swift | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/SearchBar.swift b/apple/OmnivoreKit/Sources/Views/SearchBar.swift index 81edaa3c6..7c7e02a4f 100644 --- a/apple/OmnivoreKit/Sources/Views/SearchBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SearchBar.swift @@ -26,26 +26,31 @@ public struct SearchBar: View { .padding(.leading, 10) if self.searchTerm != "" { - Button(action: { - self.searchTerm = "" - self.isFocused = false - }) { - Image(systemName: "multiply.circle.fill") - .foregroundColor(.gray) - .padding(.trailing, 8) - } + Button( + action: { + self.searchTerm = "" + }, + label: { + Image(systemName: "multiply.circle.fill") + .foregroundColor(.gray) + .padding(.trailing, 8) + } + ) } } ) .padding(.horizontal, 10) if isFocused { - Button(action: { - self.isFocused = false - self.searchTerm = "" - }) { - Text("Cancel") - } + Button( + action: { + self.searchTerm = "" + self.isFocused = false + }, + label: { + Text("Cancel") + } + ) .padding(.trailing, 10) .transition(.move(edge: .trailing)) }