Update filters

This commit is contained in:
Jackson Harper 2023-11-30 19:07:30 +08:00
parent 1bff4cbd18
commit 26d6702c6f
9 changed files with 488 additions and 156 deletions

View file

@ -12,18 +12,10 @@ class FetcherFilterState: ObservableObject {
@Published var selectedLabels = [LinkedItemLabel]()
@Published var negatedLabels = [LinkedItemLabel]()
@Published var appliedFilter: InternalFilter?
@Published var appliedSort = LinkedItemSort.newest.rawValue
@Published var appliedFilter: InternalFilter? {
didSet {
let newValue = appliedFilter?.name.lowercased()
UserDefaults.standard.setValue(newValue, forKey: "lastSelected-\(folder)-filter")
}
}
init(folder: String) {
self.folder = folder
let newValue = appliedFilter?.name.lowercased()
let appliedFilterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter")
}
}

View file

@ -43,20 +43,19 @@ struct AnimatingCellHeight: AnimatableModifier {
@ObservedObject var viewModel: HomeFeedViewModel
@State private var selection = Set<String>()
@ObservedObject var filterState: FetcherFilterState
func loadItems(isRefresh: Bool) {
Task { await viewModel.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) }
Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) }
}
var showFeatureCards: Bool {
viewModel.listConfig.hasFeatureCards &&
!viewModel.hideFeatureSection &&
viewModel.fetcher.items.count > 0 &&
filterState.searchTerm.isEmpty &&
filterState.selectedLabels.isEmpty &&
filterState.negatedLabels.isEmpty &&
filterState.appliedFilter?.name == "inbox"
viewModel.filterState.searchTerm.isEmpty &&
viewModel.filterState.selectedLabels.isEmpty &&
viewModel.filterState.negatedLabels.isEmpty &&
viewModel.filterState.appliedFilter?.name == "inbox"
}
var body: some View {
@ -67,27 +66,26 @@ struct AnimatingCellHeight: AnimatableModifier {
isEditMode: $isEditMode,
selection: $selection,
viewModel: viewModel,
filterState: filterState,
showFeatureCards: showFeatureCards
)
.refreshable {
loadItems(isRefresh: true)
}
.onChange(of: filterState.searchTerm) { _ in
.onChange(of: viewModel.filterState.searchTerm) { _ in
// Maybe we should debounce this, but
// it feels like it works ok without
loadItems(isRefresh: true)
}
.onChange(of: filterState.selectedLabels) { _ in
.onChange(of: viewModel.filterState.selectedLabels) { _ in
loadItems(isRefresh: true)
}
.onChange(of: filterState.negatedLabels) { _ in
.onChange(of: viewModel.filterState.negatedLabels) { _ in
loadItems(isRefresh: true)
}
.onChange(of: filterState.appliedFilter) { _ in
.onChange(of: viewModel.filterState.appliedFilter) { _ in
loadItems(isRefresh: true)
}
.onChange(of: filterState.appliedSort) { _ in
.onChange(of: viewModel.filterState.appliedSort) { _ in
loadItems(isRefresh: true)
}
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
@ -127,10 +125,10 @@ struct AnimatingCellHeight: AnimatableModifier {
if let deepLink = DeepLink.make(from: url) {
switch deepLink {
case let .search(query):
filterState.searchTerm = query
viewModel.filterState.searchTerm = query
case let .savedSearch(named):
if let filter = viewModel.findFilter(dataService, named: named) {
filterState.appliedFilter = filter
viewModel.filterState.appliedFilter = filter
}
case let .webAppLinkRequest(requestID):
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
@ -158,6 +156,7 @@ struct AnimatingCellHeight: AnimatableModifier {
if viewModel.fetcher.items.isEmpty {
loadItems(isRefresh: false)
}
await viewModel.loadFilters(dataService: dataService, filterState: viewModel.filterState)
}
.environment(\.editMode, self.$isEditMode)
}
@ -167,7 +166,7 @@ struct AnimatingCellHeight: AnimatableModifier {
ToolbarItem(placement: .barLeading) {
VStack(alignment: .leading) {
let showDate = isListScrolled && !listTitle.isEmpty
if let title = filterState.appliedFilter?.name {
if let title = viewModel.filterState.appliedFilter?.name {
Text(title)
.font(Font.system(size: showDate ? 10 : 18, weight: .semibold))
if showDate, prefersListLayout, isListScrolled || !showFeatureCards {
@ -264,7 +263,6 @@ struct AnimatingCellHeight: AnimatableModifier {
@Binding var isEditMode: EditMode
@Binding var selection: Set<String>
@ObservedObject var viewModel: HomeFeedViewModel
@ObservedObject var filterState: FetcherFilterState
let showFeatureCards: Bool
@ -294,23 +292,21 @@ struct AnimatingCellHeight: AnimatableModifier {
isEditMode: $isEditMode,
selection: $selection,
viewModel: viewModel,
filterState: filterState,
showFeatureCards: showFeatureCards
)
} else {
HomeFeedGridView(
viewModel: viewModel,
filterState: filterState,
isListScrolled: $isListScrolled
)
}
}.sheet(isPresented: $viewModel.showLabelsSheet) {
FilterByLabelsView(
initiallySelected: filterState.selectedLabels,
initiallyNegated: filterState.negatedLabels
initiallySelected: viewModel.filterState.selectedLabels,
initiallyNegated: viewModel.filterState.negatedLabels
) {
self.filterState.selectedLabels = $0
self.filterState.negatedLabels = $1
viewModel.filterState.selectedLabels = $0
viewModel.filterState.negatedLabels = $1
}
}
.popup(isPresented: $viewModel.showSnackbar) {
@ -351,7 +347,6 @@ struct AnimatingCellHeight: AnimatableModifier {
@Binding var selection: Set<String>
@ObservedObject var viewModel: HomeFeedViewModel
@ObservedObject var filterState: FetcherFilterState
let showFeatureCards: Bool
@ -359,20 +354,20 @@ struct AnimatingCellHeight: AnimatableModifier {
GeometryReader { reader in
ScrollView(.horizontal, showsIndicators: false) {
HStack {
if filterState.searchTerm.count > 0 {
TextChipButton.makeSearchFilterButton(title: filterState.searchTerm) {
filterState.searchTerm = ""
if viewModel.filterState.searchTerm.count > 0 {
TextChipButton.makeSearchFilterButton(title: viewModel.filterState.searchTerm) {
viewModel.filterState.searchTerm = ""
}.frame(maxWidth: reader.size.width * 0.66)
} else {
Menu(
content: {
ForEach(viewModel.filters) { filter in
Button(filter.name, action: { filterState.appliedFilter = filter })
Button(filter.name, action: { viewModel.filterState.appliedFilter = filter })
}
},
label: {
TextChipButton.makeMenuButton(
title: filterState.appliedFilter?.name ?? "-",
title: viewModel.filterState.appliedFilter?.name ?? "-",
color: .systemGray6
)
}
@ -381,25 +376,25 @@ struct AnimatingCellHeight: AnimatableModifier {
Menu(
content: {
ForEach(LinkedItemSort.allCases, id: \.self) { sort in
Button(sort.displayName, action: { filterState.appliedSort = sort.rawValue })
Button(sort.displayName, action: { viewModel.filterState.appliedSort = sort.rawValue })
}
},
label: {
TextChipButton.makeMenuButton(
title: LinkedItemSort(rawValue: filterState.appliedSort)?.displayName ?? "Sort",
title: LinkedItemSort(rawValue: viewModel.filterState.appliedSort)?.displayName ?? "Sort",
color: .systemGray6
)
}
)
TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true })
ForEach(filterState.selectedLabels, id: \.self) { label in
ForEach(viewModel.filterState.selectedLabels, id: \.self) { label in
TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) {
filterState.selectedLabels.removeAll { $0.id == label.id }
viewModel.filterState.selectedLabels.removeAll { $0.id == label.id }
}
}
ForEach(filterState.negatedLabels, id: \.self) { label in
ForEach(viewModel.filterState.negatedLabels, id: \.self) { label in
TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) {
filterState.negatedLabels.removeAll { $0.id == label.id }
viewModel.filterState.negatedLabels.removeAll { $0.id == label.id }
}
}
Spacer()
@ -569,7 +564,7 @@ struct AnimatingCellHeight: AnimatableModifier {
.listRowSeparator(.hidden, edges: .all)
.listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 0, trailing: horizontalInset))
if let appliedFilter = filterState.appliedFilter,
if let appliedFilter = viewModel.filterState.appliedFilter,
networkMonitor.status == .disconnected,
!appliedFilter.allowLocalFetch
{
@ -706,7 +701,6 @@ struct AnimatingCellHeight: AnimatableModifier {
@State var isContextMenuOpen = false
@ObservedObject var viewModel: HomeFeedViewModel
@ObservedObject var filterState: FetcherFilterState
@Binding var isListScrolled: Bool
@ -726,27 +720,27 @@ struct AnimatingCellHeight: AnimatableModifier {
}
func loadItems(isRefresh: Bool) {
Task { await viewModel.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) }
Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) }
}
var filtersHeader: some View {
GeometryReader { reader in
ScrollView(.horizontal, showsIndicators: false) {
HStack {
if filterState.searchTerm.count > 0 {
TextChipButton.makeSearchFilterButton(title: filterState.searchTerm) {
filterState.searchTerm = ""
if viewModel.filterState.searchTerm.count > 0 {
TextChipButton.makeSearchFilterButton(title: viewModel.filterState.searchTerm) {
viewModel.filterState.searchTerm = ""
}.frame(maxWidth: reader.size.width * 0.66)
} else {
Menu(
content: {
ForEach(viewModel.filters, id: \.self) { filter in
Button(filter.name, action: { filterState.appliedFilter = filter })
Button(filter.name, action: { viewModel.filterState.appliedFilter = filter })
}
},
label: {
TextChipButton.makeMenuButton(
title: filterState.appliedFilter?.name ?? "-",
title: viewModel.filterState.appliedFilter?.name ?? "-",
color: .systemGray6
)
}
@ -755,25 +749,25 @@ struct AnimatingCellHeight: AnimatableModifier {
Menu(
content: {
ForEach(LinkedItemSort.allCases, id: \.self) { sort in
Button(sort.displayName, action: { filterState.appliedSort = sort.rawValue })
Button(sort.displayName, action: { viewModel.filterState.appliedSort = sort.rawValue })
}
},
label: {
TextChipButton.makeMenuButton(
title: LinkedItemSort(rawValue: filterState.appliedSort)?.displayName ?? "Sort",
title: LinkedItemSort(rawValue: viewModel.filterState.appliedSort)?.displayName ?? "Sort",
color: .systemGray6
)
}
)
TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true })
ForEach(filterState.selectedLabels, id: \.self) { label in
ForEach(viewModel.filterState.selectedLabels, id: \.self) { label in
TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) {
filterState.selectedLabels.removeAll { $0.id == label.id }
viewModel.filterState.selectedLabels.removeAll { $0.id == label.id }
}
}
ForEach(filterState.negatedLabels, id: \.self) { label in
ForEach(viewModel.filterState.negatedLabels, id: \.self) { label in
TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) {
filterState.negatedLabels.removeAll { $0.id == label.id }
viewModel.filterState.negatedLabels.removeAll { $0.id == label.id }
}
}
Spacer()

View file

@ -37,14 +37,17 @@ import Views
@Published var filters = [InternalFilter]()
@ObservedObject var filterState: FetcherFilterState
@AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false
@AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue
let fetcher: LibraryItemFetcher
init(fetcher: LibraryItemFetcher, listConfig: LibraryListConfig) {
init(fetcher: LibraryItemFetcher, filterState: FetcherFilterState, listConfig: LibraryListConfig) {
self.fetcher = fetcher
self.listConfig = listConfig
self.filterState = filterState
super.init()
}
@ -64,7 +67,32 @@ import Views
}
}
func itemAppeared(item: Models.LibraryItem, dataService _: DataService) async {
func loadFilters(dataService: DataService, filterState: FetcherFilterState) async {
switch filterState.folder {
case "following":
updateFilters(filterState: filterState, newFilters: InternalFilter.DefaultFollowingFilters)
default:
var hasLocalResults = false
let fetchRequest: NSFetchRequest<Models.Filter> = Filter.fetchRequest()
// Load from disk
if let results = try? dataService.viewContext.fetch(fetchRequest) {
hasLocalResults = true
updateFilters(filterState: filterState, newFilters: InternalFilter.make(from: results))
}
let hasResults = hasLocalResults
Task.detached {
if let downloadedFilters = try? await dataService.filters() {
await self.updateFilters(filterState: filterState, newFilters: downloadedFilters)
} else if !hasResults {
await self.updateFilters(filterState: filterState, newFilters: InternalFilter.DefaultInboxFilters)
}
}
}
}
func itemAppeared(item: Models.LibraryItem, dataService: DataService) async {
if isLoading { return }
let itemIndex = fetcher.items.firstIndex(where: { $0.id == item.id })
let thresholdIndex = fetcher.items.index(fetcher.items.endIndex, offsetBy: -5)
@ -72,9 +100,9 @@ import Views
// Check if user has scrolled to the last five items in the list
// Make sure we aren't currently loading though, as this would get triggered when the first set
// of items are presented to the user.
// if let itemIndex = itemIndex, itemIndex > thresholdIndex {
// await loadMoreItems(dataService: dataService, isRefresh: false)
// }
if let itemIndex = itemIndex, itemIndex > thresholdIndex {
await loadMoreItems(dataService: dataService, filterState: filterState, isRefresh: false)
}
}
func pushFeedItem(item _: Models.LibraryItem) {
@ -98,38 +126,23 @@ import Views
}
}
func loadFilters(dataService: DataService) async {
var hasLocalResults = false
let fetchRequest: NSFetchRequest<Models.Filter> = Filter.fetchRequest()
func updateFilters(filterState: FetcherFilterState, newFilters: [InternalFilter]) {
let appliedFilterName = UserDefaults.standard.string(forKey: "lastSelected-\(filterState.folder)-filter") ?? filterState.folder
// Load from disk
if let results = try? dataService.viewContext.fetch(fetchRequest) {
hasLocalResults = true
updateFilters(newFilters: InternalFilter.make(from: results))
}
filters = newFilters
.filter { $0.folder == filterState.folder }
.sorted(by: { $0.position < $1.position })
+ [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter]
let hasResults = hasLocalResults
Task.detached {
if let downloadedFilters = try? await dataService.filters() {
await self.updateFilters(newFilters: downloadedFilters)
} else if !hasResults {
await self.updateFilters(newFilters: InternalFilter.DefaultFilters)
}
if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != filterState.appliedFilter?.id {
filterState.appliedFilter = newFilter
}
}
func updateFilters(newFilters _: [InternalFilter]) {
// filters = newFilters.sorted(by: { $0.position < $1.position }) + [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter]
// if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id {
// appliedFilter = newFilter
// }
}
func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async {
func loadItems(dataService: DataService, isRefresh: Bool) async {
isLoading = true
showLoadingBar = true
// group.addTask { await self.loadFilters(dataService: dataService) }
await fetcher.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh)
updateFeatureFilter(context: dataService.viewContext, filter: FeaturedItemFilter(rawValue: featureFilter))

View file

@ -6,17 +6,13 @@ import Views
struct HomeView: View {
@State private var viewModel: HomeFeedViewModel
let inboxFilterState = FetcherFilterState(
folder: "inbox"
)
init(viewModel: HomeFeedViewModel) {
self.viewModel = viewModel
}
var body: some View {
#if os(iOS)
HomeFeedContainerView(viewModel: viewModel, filterState: inboxFilterState)
HomeFeedContainerView(viewModel: viewModel)
#elseif os(macOS)
HomeFeedView(viewModel: viewModel)
.frame(minWidth: 320)

View file

@ -12,6 +12,7 @@ import SwiftUI
struct LibraryListView: View {
@StateObject private var libraryViewModel = HomeFeedViewModel(
fetcher: LibraryItemFetcher(),
filterState: FetcherFilterState(folder: "inbox"),
listConfig: LibraryListConfig(
hasFeatureCards: true,
leadingSwipeActions: [.pin],

View file

@ -25,6 +25,7 @@ struct LibraryTabView: View {
@StateObject private var followingViewModel = HomeFeedViewModel(
fetcher: LibraryItemFetcher(),
filterState: FetcherFilterState(folder: "following"),
listConfig: LibraryListConfig(
hasFeatureCards: false,
leadingSwipeActions: [.moveToInbox],
@ -35,6 +36,7 @@ struct LibraryTabView: View {
@StateObject private var libraryViewModel = HomeFeedViewModel(
fetcher: LibraryItemFetcher(),
filterState: FetcherFilterState(folder: "inbox"),
listConfig: LibraryListConfig(
hasFeatureCards: true,
leadingSwipeActions: [.pin],
@ -47,19 +49,13 @@ struct LibraryTabView: View {
VStack(spacing: 0) {
TabView(selection: $selectedTab) {
NavigationView {
HomeFeedContainerView(
viewModel: followingViewModel,
filterState: FetcherFilterState(folder: "following")
)
.navigationViewStyle(.stack)
HomeFeedContainerView(viewModel: followingViewModel)
.navigationViewStyle(.stack)
}.tag("following")
NavigationView {
HomeFeedContainerView(
viewModel: libraryViewModel,
filterState: FetcherFilterState(folder: "inbox")
)
.navigationViewStyle(.stack)
HomeFeedContainerView(viewModel: libraryViewModel)
.navigationViewStyle(.stack)
}.tag("inbox")
NavigationView {

View file

@ -5643,6 +5643,7 @@ extension Objects {
let image: [String: String]
let publishedAt: [String: DateTime]
let title: [String: String]
let type: [String: String]
let updatedAt: [String: DateTime]
let url: [String: String]
@ -5692,6 +5693,10 @@ extension Objects.Feed: Decodable {
if let value = try container.decode(String?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "type":
if let value = try container.decode(String?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "updatedAt":
if let value = try container.decode(DateTime?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@ -5717,6 +5722,7 @@ extension Objects.Feed: Decodable {
image = map["image"]
publishedAt = map["publishedAt"]
title = map["title"]
type = map["type"]
updatedAt = map["updatedAt"]
url = map["url"]
}
@ -5738,7 +5744,7 @@ extension Fields where TypeLock == Objects.Feed {
}
}
func createdAt() throws -> DateTime {
func createdAt() throws -> DateTime? {
let field = GraphQLField.leaf(
name: "createdAt",
arguments: []
@ -5747,12 +5753,9 @@ extension Fields where TypeLock == Objects.Feed {
switch response {
case let .decoding(data):
if let data = data.createdAt[field.alias!] {
return data
}
throw HttpError.badpayload
return data.createdAt[field.alias!]
case .mocking:
return DateTime.mockValue
return nil
}
}
@ -5771,7 +5774,7 @@ extension Fields where TypeLock == Objects.Feed {
}
}
func id() throws -> String {
func id() throws -> String? {
let field = GraphQLField.leaf(
name: "id",
arguments: []
@ -5780,12 +5783,9 @@ extension Fields where TypeLock == Objects.Feed {
switch response {
case let .decoding(data):
if let data = data.id[field.alias!] {
return data
}
throw HttpError.badpayload
return data.id[field.alias!]
case .mocking:
return String.mockValue
return nil
}
}
@ -5837,7 +5837,22 @@ extension Fields where TypeLock == Objects.Feed {
}
}
func updatedAt() throws -> DateTime {
func type() throws -> String? {
let field = GraphQLField.leaf(
name: "type",
arguments: []
)
select(field)
switch response {
case let .decoding(data):
return data.type[field.alias!]
case .mocking:
return nil
}
}
func updatedAt() throws -> DateTime? {
let field = GraphQLField.leaf(
name: "updatedAt",
arguments: []
@ -5846,12 +5861,9 @@ extension Fields where TypeLock == Objects.Feed {
switch response {
case let .decoding(data):
if let data = data.updatedAt[field.alias!] {
return data
}
throw HttpError.badpayload
return data.updatedAt[field.alias!]
case .mocking:
return DateTime.mockValue
return nil
}
}
@ -6643,11 +6655,11 @@ extension Selection where TypeLock == Never, Type == Never {
extension Objects {
struct Filter {
let __typename: TypeName = .filter
let category: [String: String]
let createdAt: [String: DateTime]
let defaultFilter: [String: Bool]
let description: [String: String]
let filter: [String: String]
let folder: [String: String]
let id: [String: String]
let name: [String: String]
let position: [String: Int]
@ -6672,10 +6684,6 @@ extension Objects.Filter: Decodable {
let field = GraphQLField.getFieldNameFromAlias(alias)
switch field {
case "category":
if let value = try container.decode(String?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "createdAt":
if let value = try container.decode(DateTime?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@ -6692,6 +6700,10 @@ extension Objects.Filter: Decodable {
if let value = try container.decode(String?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "folder":
if let value = try container.decode(String?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "id":
if let value = try container.decode(String?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@ -6722,11 +6734,11 @@ extension Objects.Filter: Decodable {
}
}
category = map["category"]
createdAt = map["createdAt"]
defaultFilter = map["defaultFilter"]
description = map["description"]
filter = map["filter"]
folder = map["folder"]
id = map["id"]
name = map["name"]
position = map["position"]
@ -6736,24 +6748,6 @@ extension Objects.Filter: Decodable {
}
extension Fields where TypeLock == Objects.Filter {
func category() throws -> String {
let field = GraphQLField.leaf(
name: "category",
arguments: []
)
select(field)
switch response {
case let .decoding(data):
if let data = data.category[field.alias!] {
return data
}
throw HttpError.badpayload
case .mocking:
return String.mockValue
}
}
func createdAt() throws -> DateTime {
let field = GraphQLField.leaf(
name: "createdAt",
@ -6820,6 +6814,24 @@ extension Fields where TypeLock == Objects.Filter {
}
}
func folder() throws -> String {
let field = GraphQLField.leaf(
name: "folder",
arguments: []
)
select(field)
switch response {
case let .decoding(data):
if let data = data.folder[field.alias!] {
return data
}
throw HttpError.badpayload
case .mocking:
return String.mockValue
}
}
func id() throws -> String {
let field = GraphQLField.leaf(
name: "id",
@ -13695,6 +13707,7 @@ extension Objects {
let recentEmails: [String: Unions.RecentEmailsResult]
let recentSearches: [String: Unions.RecentSearchesResult]
let rules: [String: Unions.RulesResult]
let scanFeeds: [String: Unions.ScanFeedsResult]
let search: [String: Unions.SearchResult]
let sendInstallInstructions: [String: Unions.SendInstallInstructionsResult]
let subscriptions: [String: Unions.SubscriptionsResult]
@ -13788,6 +13801,10 @@ extension Objects.Query: Decodable {
if let value = try container.decode(Unions.RulesResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "scanFeeds":
if let value = try container.decode(Unions.ScanFeedsResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "search":
if let value = try container.decode(Unions.SearchResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@ -13854,6 +13871,7 @@ extension Objects.Query: Decodable {
recentEmails = map["recentEmails"]
recentSearches = map["recentSearches"]
rules = map["rules"]
scanFeeds = map["scanFeeds"]
search = map["search"]
sendInstallInstructions = map["sendInstallInstructions"]
subscriptions = map["subscriptions"]
@ -14165,6 +14183,25 @@ extension Fields where TypeLock == Objects.Query {
}
}
func scanFeeds<Type>(input: InputObjects.ScanFeedsInput, selection: Selection<Type, Unions.ScanFeedsResult>) throws -> Type {
let field = GraphQLField.composite(
name: "scanFeeds",
arguments: [Argument(name: "input", type: "ScanFeedsInput!", value: input)],
selection: selection.selection
)
select(field)
switch response {
case let .decoding(data):
if let data = data.scanFeeds[field.alias!] {
return try selection.decode(data: data)
}
throw HttpError.badpayload
case .mocking:
return selection.mock()
}
}
func search<Type>(after: OptionalArgument<String> = .absent(), first: OptionalArgument<Int> = .absent(), format: OptionalArgument<String> = .absent(), includeContent: OptionalArgument<Bool> = .absent(), query: OptionalArgument<String> = .absent(), selection: Selection<Type, Unions.SearchResult>) throws -> Type {
let field = GraphQLField.composite(
name: "search",
@ -14241,10 +14278,10 @@ extension Fields where TypeLock == Objects.Query {
}
}
func updatesSince<Type>(after: OptionalArgument<String> = .absent(), first: OptionalArgument<Int> = .absent(), since: DateTime, sort: OptionalArgument<InputObjects.SortParams> = .absent(), selection: Selection<Type, Unions.UpdatesSinceResult>) throws -> Type {
func updatesSince<Type>(after: OptionalArgument<String> = .absent(), first: OptionalArgument<Int> = .absent(), folder: OptionalArgument<String> = .absent(), since: DateTime, sort: OptionalArgument<InputObjects.SortParams> = .absent(), selection: Selection<Type, Unions.UpdatesSinceResult>) throws -> Type {
let field = GraphQLField.composite(
name: "updatesSince",
arguments: [Argument(name: "after", type: "String", value: after), Argument(name: "first", type: "Int", value: first), Argument(name: "since", type: "Date!", value: since), Argument(name: "sort", type: "SortParams", value: sort)],
arguments: [Argument(name: "after", type: "String", value: after), Argument(name: "first", type: "Int", value: first), Argument(name: "folder", type: "String", value: folder), Argument(name: "since", type: "Date!", value: since), Argument(name: "sort", type: "SortParams", value: sort)],
selection: selection.selection
)
select(field)
@ -17454,6 +17491,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias SaveSuccess<T> = Selection<T, Objects.SaveSuccess>
}
extension Objects {
struct ScanFeedsError {
let __typename: TypeName = .scanFeedsError
let errorCodes: [String: [Enums.ScanFeedsErrorCode]]
enum TypeName: String, Codable {
case scanFeedsError = "ScanFeedsError"
}
}
}
extension Objects.ScanFeedsError: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var map = HashMap()
for codingKey in container.allKeys {
if codingKey.isTypenameKey { continue }
let alias = codingKey.stringValue
let field = GraphQLField.getFieldNameFromAlias(alias)
switch field {
case "errorCodes":
if let value = try container.decode([Enums.ScanFeedsErrorCode]?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
default:
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Unknown key \(field)."
)
)
}
}
errorCodes = map["errorCodes"]
}
}
extension Fields where TypeLock == Objects.ScanFeedsError {
func errorCodes() throws -> [Enums.ScanFeedsErrorCode] {
let field = GraphQLField.leaf(
name: "errorCodes",
arguments: []
)
select(field)
switch response {
case let .decoding(data):
if let data = data.errorCodes[field.alias!] {
return data
}
throw HttpError.badpayload
case .mocking:
return []
}
}
}
extension Selection where TypeLock == Never, Type == Never {
typealias ScanFeedsError<T> = Selection<T, Objects.ScanFeedsError>
}
extension Objects {
struct ScanFeedsSuccess {
let __typename: TypeName = .scanFeedsSuccess
let feeds: [String: [Objects.Feed]]
enum TypeName: String, Codable {
case scanFeedsSuccess = "ScanFeedsSuccess"
}
}
}
extension Objects.ScanFeedsSuccess: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var map = HashMap()
for codingKey in container.allKeys {
if codingKey.isTypenameKey { continue }
let alias = codingKey.stringValue
let field = GraphQLField.getFieldNameFromAlias(alias)
switch field {
case "feeds":
if let value = try container.decode([Objects.Feed]?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
default:
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Unknown key \(field)."
)
)
}
}
feeds = map["feeds"]
}
}
extension Fields where TypeLock == Objects.ScanFeedsSuccess {
func feeds<Type>(selection: Selection<Type, [Objects.Feed]>) throws -> Type {
let field = GraphQLField.composite(
name: "feeds",
arguments: [],
selection: selection.selection
)
select(field)
switch response {
case let .decoding(data):
if let data = data.feeds[field.alias!] {
return try selection.decode(data: data)
}
throw HttpError.badpayload
case .mocking:
return selection.mock()
}
}
}
extension Selection where TypeLock == Never, Type == Never {
typealias ScanFeedsSuccess<T> = Selection<T, Objects.ScanFeedsSuccess>
}
extension Objects {
struct SearchError {
let __typename: TypeName = .searchError
@ -29856,6 +30024,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias SaveResult<T> = Selection<T, Unions.SaveResult>
}
extension Unions {
struct ScanFeedsResult {
let __typename: TypeName
let errorCodes: [String: [Enums.ScanFeedsErrorCode]]
let feeds: [String: [Objects.Feed]]
enum TypeName: String, Codable {
case scanFeedsError = "ScanFeedsError"
case scanFeedsSuccess = "ScanFeedsSuccess"
}
}
}
extension Unions.ScanFeedsResult: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var map = HashMap()
for codingKey in container.allKeys {
if codingKey.isTypenameKey { continue }
let alias = codingKey.stringValue
let field = GraphQLField.getFieldNameFromAlias(alias)
switch field {
case "errorCodes":
if let value = try container.decode([Enums.ScanFeedsErrorCode]?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
case "feeds":
if let value = try container.decode([Objects.Feed]?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
default:
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Unknown key \(field)."
)
)
}
}
__typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
errorCodes = map["errorCodes"]
feeds = map["feeds"]
}
}
extension Fields where TypeLock == Unions.ScanFeedsResult {
func on<Type>(scanFeedsError: Selection<Type, Objects.ScanFeedsError>, scanFeedsSuccess: Selection<Type, Objects.ScanFeedsSuccess>) throws -> Type {
select([GraphQLField.fragment(type: "ScanFeedsError", selection: scanFeedsError.selection), GraphQLField.fragment(type: "ScanFeedsSuccess", selection: scanFeedsSuccess.selection)])
switch response {
case let .decoding(data):
switch data.__typename {
case .scanFeedsError:
let data = Objects.ScanFeedsError(errorCodes: data.errorCodes)
return try scanFeedsError.decode(data: data)
case .scanFeedsSuccess:
let data = Objects.ScanFeedsSuccess(feeds: data.feeds)
return try scanFeedsSuccess.decode(data: data)
}
case .mocking:
return scanFeedsError.mock()
}
}
}
extension Selection where TypeLock == Never, Type == Never {
typealias ScanFeedsResult<T> = Selection<T, Unions.ScanFeedsResult>
}
extension Unions {
struct SearchResult {
let __typename: TypeName
@ -33434,6 +33676,22 @@ extension Enums {
}
}
extension Enums {
/// ScanFeedsErrorCode
enum ScanFeedsErrorCode: String, CaseIterable, Codable {
case badRequest = "BAD_REQUEST"
}
}
extension Enums {
/// ScanFeedsType
enum ScanFeedsType: String, CaseIterable, Codable {
case html = "HTML"
case opml = "OPML"
}
}
extension Enums {
/// SearchErrorCode
enum SearchErrorCode: String, CaseIterable, Codable {
@ -33997,6 +34255,8 @@ extension InputObjects {
struct CreateArticleInput: Encodable, Hashable {
var articleSavingRequestId: OptionalArgument<String> = .absent()
var folder: OptionalArgument<String> = .absent()
var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent()
var preparedDocument: OptionalArgument<InputObjects.PreparedDocumentInput> = .absent()
@ -34014,6 +34274,7 @@ extension InputObjects {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if articleSavingRequestId.hasValue { try container.encode(articleSavingRequestId, forKey: .articleSavingRequestId) }
if folder.hasValue { try container.encode(folder, forKey: .folder) }
if labels.hasValue { try container.encode(labels, forKey: .labels) }
if preparedDocument.hasValue { try container.encode(preparedDocument, forKey: .preparedDocument) }
if skipParsing.hasValue { try container.encode(skipParsing, forKey: .skipParsing) }
@ -34025,6 +34286,7 @@ extension InputObjects {
enum CodingKeys: String, CodingKey {
case articleSavingRequestId
case folder
case labels
case preparedDocument
case skipParsing
@ -34733,6 +34995,8 @@ extension InputObjects {
struct SaveFileInput: Encodable, Hashable {
var clientRequestId: String
var folder: OptionalArgument<String> = .absent()
var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent()
var source: String
@ -34746,6 +35010,7 @@ extension InputObjects {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(clientRequestId, forKey: .clientRequestId)
if folder.hasValue { try container.encode(folder, forKey: .folder) }
if labels.hasValue { try container.encode(labels, forKey: .labels) }
try container.encode(source, forKey: .source)
if state.hasValue { try container.encode(state, forKey: .state) }
@ -34755,6 +35020,7 @@ extension InputObjects {
enum CodingKeys: String, CodingKey {
case clientRequestId
case folder
case labels
case source
case state
@ -34766,29 +35032,29 @@ extension InputObjects {
extension InputObjects {
struct SaveFilterInput: Encodable, Hashable {
var category: OptionalArgument<String> = .absent()
var description: OptionalArgument<String> = .absent()
var filter: String
var folder: OptionalArgument<String> = .absent()
var name: String
var position: OptionalArgument<Int> = .absent()
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if category.hasValue { try container.encode(category, forKey: .category) }
if description.hasValue { try container.encode(description, forKey: .description) }
try container.encode(filter, forKey: .filter)
if folder.hasValue { try container.encode(folder, forKey: .folder) }
try container.encode(name, forKey: .name)
if position.hasValue { try container.encode(position, forKey: .position) }
}
enum CodingKeys: String, CodingKey {
case category
case description
case filter
case folder
case name
case position
}
@ -34799,6 +35065,8 @@ extension InputObjects {
struct SavePageInput: Encodable, Hashable {
var clientRequestId: String
var folder: OptionalArgument<String> = .absent()
var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent()
var originalContent: String
@ -34822,6 +35090,7 @@ extension InputObjects {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(clientRequestId, forKey: .clientRequestId)
if folder.hasValue { try container.encode(folder, forKey: .folder) }
if labels.hasValue { try container.encode(labels, forKey: .labels) }
try container.encode(originalContent, forKey: .originalContent)
if parseResult.hasValue { try container.encode(parseResult, forKey: .parseResult) }
@ -34836,6 +35105,7 @@ extension InputObjects {
enum CodingKeys: String, CodingKey {
case clientRequestId
case folder
case labels
case originalContent
case parseResult
@ -34854,6 +35124,8 @@ extension InputObjects {
struct SaveUrlInput: Encodable, Hashable {
var clientRequestId: String
var folder: OptionalArgument<String> = .absent()
var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent()
var locale: OptionalArgument<String> = .absent()
@ -34873,6 +35145,7 @@ extension InputObjects {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(clientRequestId, forKey: .clientRequestId)
if folder.hasValue { try container.encode(folder, forKey: .folder) }
if labels.hasValue { try container.encode(labels, forKey: .labels) }
if locale.hasValue { try container.encode(locale, forKey: .locale) }
if publishedAt.hasValue { try container.encode(publishedAt, forKey: .publishedAt) }
@ -34885,6 +35158,7 @@ extension InputObjects {
enum CodingKeys: String, CodingKey {
case clientRequestId
case folder
case labels
case locale
case publishedAt
@ -34897,6 +35171,29 @@ extension InputObjects {
}
}
extension InputObjects {
struct ScanFeedsInput: Encodable, Hashable {
var opml: OptionalArgument<String> = .absent()
var type: Enums.ScanFeedsType
var url: OptionalArgument<String> = .absent()
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if opml.hasValue { try container.encode(opml, forKey: .opml) }
try container.encode(type, forKey: .type)
if url.hasValue { try container.encode(url, forKey: .url) }
}
enum CodingKeys: String, CodingKey {
case opml
case type
case url
}
}
}
extension InputObjects {
struct SetBookmarkArticleInput: Encodable, Hashable {
var articleId: String
@ -34966,6 +35263,8 @@ extension InputObjects {
var syncedAt: OptionalArgument<DateTime> = .absent()
var taskName: OptionalArgument<String> = .absent()
var token: String
var type: OptionalArgument<Enums.IntegrationType> = .absent()
@ -34977,6 +35276,7 @@ extension InputObjects {
if importItemState.hasValue { try container.encode(importItemState, forKey: .importItemState) }
try container.encode(name, forKey: .name)
if syncedAt.hasValue { try container.encode(syncedAt, forKey: .syncedAt) }
if taskName.hasValue { try container.encode(taskName, forKey: .taskName) }
try container.encode(token, forKey: .token)
if type.hasValue { try container.encode(type, forKey: .type) }
}
@ -34987,6 +35287,7 @@ extension InputObjects {
case importItemState
case name
case syncedAt
case taskName
case token
case type
}
@ -35277,12 +35578,12 @@ extension InputObjects {
extension InputObjects {
struct UpdateFilterInput: Encodable, Hashable {
var category: OptionalArgument<String> = .absent()
var description: OptionalArgument<String> = .absent()
var filter: OptionalArgument<String> = .absent()
var folder: OptionalArgument<String> = .absent()
var id: String
var name: OptionalArgument<String> = .absent()
@ -35293,9 +35594,9 @@ extension InputObjects {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if category.hasValue { try container.encode(category, forKey: .category) }
if description.hasValue { try container.encode(description, forKey: .description) }
if filter.hasValue { try container.encode(filter, forKey: .filter) }
if folder.hasValue { try container.encode(folder, forKey: .folder) }
try container.encode(id, forKey: .id)
if name.hasValue { try container.encode(name, forKey: .name) }
if position.hasValue { try container.encode(position, forKey: .position) }
@ -35303,9 +35604,9 @@ extension InputObjects {
}
enum CodingKeys: String, CodingKey {
case category
case description
case filter
case folder
case id
case name
case position

View file

@ -6,6 +6,7 @@ let filterSelection = Selection.Filter {
InternalFilter(
id: try $0.id(),
name: try $0.name(),
folder: try $0.folder(),
filter: try $0.filter(),
visible: try $0.visible() ?? true,
position: try $0.position(),

View file

@ -5,6 +5,7 @@ import Models
public struct InternalFilter: Encodable, Identifiable, Hashable {
public let id: String
public let name: String
public let folder: String
public let filter: String
public let visible: Bool
public let position: Int
@ -14,6 +15,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "downloaded",
name: "Downloaded",
folder: "inbox",
filter: "",
visible: true,
position: -1,
@ -25,6 +27,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "deleted",
name: "Deleted",
folder: "inbox",
filter: "in:trash",
visible: true,
position: -1,
@ -32,11 +35,12 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
)
}
public static var DefaultFilters: [InternalFilter] {
public static var DefaultInboxFilters: [InternalFilter] {
[
InternalFilter(
id: "inbox",
name: "Inbox",
folder: "inbox",
filter: "",
visible: true,
position: 0,
@ -45,6 +49,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "non-feed-items",
name: "Non-Feed Items",
folder: "inbox",
filter: "",
visible: true,
position: 1,
@ -53,6 +58,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "newsletters",
name: "Newsletters",
folder: "inbox",
filter: "",
visible: true,
position: 2,
@ -61,6 +67,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "feeds",
name: "Feeds",
folder: "inbox",
filter: "",
visible: true,
position: 3,
@ -69,6 +76,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "archived",
name: "Archived",
folder: "inbox",
filter: "is:archived",
visible: true,
position: 4,
@ -77,6 +85,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "files",
name: "Files",
folder: "inbox",
filter: "type:file",
visible: true,
position: 5,
@ -85,6 +94,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "highlighted",
name: "Highlights",
folder: "inbox",
filter: "has:highlights",
visible: true,
position: 6,
@ -93,6 +103,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
InternalFilter(
id: "all",
name: "All",
folder: "inbox",
filter: "in:all",
visible: true,
position: 7,
@ -101,6 +112,29 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
]
}
public static var DefaultFollowingFilters: [InternalFilter] {
[
InternalFilter(
id: "following",
name: "Following",
folder: "following",
filter: "in:following",
visible: true,
position: 0,
defaultFilter: true
),
InternalFilter(
id: "rss",
name: "RSS",
folder: "following",
filter: "in:following label:RSS",
visible: true,
position: 1,
defaultFilter: true
)
]
}
public var shouldRemoteSearch: Bool {
id != "downloaded"
}
@ -182,7 +216,9 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate])
case "Deleted":
let deletedPredicate = NSPredicate(
format: "%K == %i", #keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue)
format: "%K == %i",
#keyPath(Models.LibraryItem.serverSyncStatus),
Int64(ServerSyncStatus.needsDeletion.rawValue)
)
return NSCompoundPredicate(andPredicateWithSubpredicates: [deletedPredicate])
case "Files":
@ -227,6 +263,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
let newFilter = existing ?? Filter(entity: Filter.entity(), insertInto: context)
newFilter.id = id
newFilter.name = name
newFilter.folder = folder
newFilter.filter = filter
newFilter.visible = visible
newFilter.position = Int64(position)
@ -238,11 +275,13 @@ public struct InternalFilter: Encodable, Identifiable, Hashable {
filters.compactMap { filter in
if let id = filter.id,
let name = filter.name,
let folder = filter.folder,
let filterStr = filter.filter
{
return InternalFilter(
id: id,
name: name,
folder: folder,
filter: filterStr,
visible: filter.visible,
position: Int(filter.position),
@ -264,7 +303,6 @@ public extension Filter {
)
var filter: Filter?
context.performAndWait {
filter = (try? context.fetch(fetchRequest))?.first
}