diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index a52d67cfd..477026781 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -33,7 +33,6 @@ struct FeedCardNavigationLink: View { @EnvironmentObject var audioController: AudioController let item: Models.LibraryItem - 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 65ddf6267..608551633 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -28,7 +28,8 @@ struct AnimatingCellHeight: AnimatableModifier { struct HomeFeedContainerView: View { @State var hasHighlightMutations = false @State var searchPresented = false - @State var addLinkPresented = false + @State var showAddLinkView = false + @State var showAddFeedView = false @State var isListScrolled = false @State var listTitle = "" @State var isEditMode: EditMode = .inactive @@ -70,6 +71,7 @@ struct AnimatingCellHeight: AnimatableModifier { isListScrolled: $isListScrolled, prefersListLayout: $prefersListLayout, isEditMode: $isEditMode, + showAddFeedView: $showAddFeedView, selection: $selection, viewModel: viewModel, showFeatureCards: showFeatureCards @@ -174,7 +176,7 @@ struct AnimatingCellHeight: AnimatableModifier { .fullScreenCover(isPresented: $searchPresented) { LibrarySearchView(homeFeedViewModel: self.viewModel) } - .sheet(isPresented: $addLinkPresented) { + .sheet(isPresented: $showAddLinkView) { NavigationView { LibraryAddLinkView() } @@ -188,6 +190,54 @@ struct AnimatingCellHeight: AnimatableModifier { .environment(\.editMode, self.$isEditMode) } + var trailingItems: some ToolbarContent { + Group { + ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) { + if enableGrid { + Button( + action: { prefersListLayout.toggle() }, + label: { + Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") + } + ) + } + } + ToolbarItem(placement: .barTrailing) { + Button( + action: { + if viewModel.folder == "inbox" { + showAddLinkView = true + } else if viewModel.folder == "following" { + showAddFeedView = true + } + }, + label: { + Image.addLink + .foregroundColor(Color.appGrayTextContrast) + } + ) + } + ToolbarItem(placement: .barTrailing) { + Button( + action: { isEditMode = isEditMode == .active ? .inactive : .active }, + label: { + Image.selectMultiple + .foregroundColor(Color.appGrayTextContrast) + } + ) + } + ToolbarItem(placement: .barTrailing) { + Button( + action: { searchPresented = true }, + label: { + Image.magnifyingGlass + .foregroundColor(Color.appGrayTextContrast) + } + ) + } + } + } + var toolbarItems: some ToolbarContent { Group { ToolbarItem(placement: .barLeading) { @@ -205,47 +255,9 @@ struct AnimatingCellHeight: AnimatableModifier { } .frame(maxWidth: .infinity, alignment: .bottomLeading) } - ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) { - if enableGrid { - Button( - action: { prefersListLayout.toggle() }, - label: { - Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") - } - ) - } - } - ToolbarItem(placement: .barTrailing) { - Button( - action: { searchPresented = true }, - label: { - Image.magnifyingGlass - .foregroundColor(Color.appGrayTextContrast) - } - ) - } - ToolbarItem(placement: .barTrailing) { - Button( - action: { isEditMode = isEditMode == .active ? .inactive : .active }, - label: { - Image.selectMultiple - .foregroundColor(Color.appGrayTextContrast) - } - ) - } - ToolbarItem(placement: .barTrailing) { - if viewModel.folder == "inbox" { - Button( - action: { addLinkPresented = true }, - label: { - Image.addLink - .foregroundColor(Color.appGrayTextContrast) - } - ) - } else { - EmptyView() - } - } + + trailingItems + ToolbarItemGroup(placement: .bottomBar) { if isEditMode == .active { Button(action: { @@ -274,6 +286,7 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isListScrolled: Bool @Binding var prefersListLayout: Bool @Binding var isEditMode: EditMode + @Binding var showAddFeedView: Bool @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel @@ -304,6 +317,7 @@ struct AnimatingCellHeight: AnimatableModifier { isListScrolled: $isListScrolled, prefersListLayout: $prefersListLayout, isEditMode: $isEditMode, + showAddFeedView: $showAddFeedView, selection: $selection, viewModel: viewModel, showFeatureCards: showFeatureCards @@ -357,7 +371,7 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isListScrolled: Bool @Binding var prefersListLayout: Bool @Binding var isEditMode: EditMode - @State private var showAddFeedView = false + @Binding var showAddFeedView: Bool @State private var showHideFeatureAlert = false @State private var showHideFollowingAlert = false @@ -632,7 +646,6 @@ struct AnimatingCellHeight: AnimatableModifier { FeedCardNavigationLink( item: item, - isInMultiSelectMode: viewModel.isInMultiSelectMode, viewModel: viewModel ) .background(GeometryReader { geometry in @@ -735,7 +748,9 @@ struct AnimatingCellHeight: AnimatableModifier { } .sheet(isPresented: $showAddFeedView) { NavigationView { - LibraryAddFeedView() + LibraryAddFeedView(dismiss: { + showAddFeedView = false + }) } } .alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.", diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 6943fa16e..54cf6fd78 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -20,7 +20,6 @@ import Views @Published var linkRequest: LinkRequest? @Published var presentWebContainer = false @Published var showLoadingBar = false - @Published var isInMultiSelectMode = false @Published var selectedLinkItem: NSManagedObjectID? // used by mac app only @Published var selectedItem: Models.LibraryItem? diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift index 420fb4cf0..f39637e54 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift @@ -5,43 +5,10 @@ import Services import SwiftUI import Views -@MainActor final class LibraryAddFeedViewModel: NSObject, ObservableObject { - @Published var isLoading = false - @Published var errorMessage: String = "" - @Published var showErrorMessage: Bool = false - - @Environment(\.dismiss) private var dismiss - - func addLink(dataService: DataService, newLinkURL: String, dismiss: DismissAction) { - isLoading = true - Task { - if URL(string: newLinkURL) == nil { - error("Invalid link") - } else { - let result = try? await dataService.saveURL(id: UUID().uuidString, url: newLinkURL) - if result == nil { - error("Error adding link") - } else { - dismiss() - } - } - isLoading = false - } - } - - func error(_ msg: String) { - errorMessage = msg - showErrorMessage = true - isLoading = false - } -} - struct LibraryAddFeedView: View { - @StateObject var viewModel = LibraryAddFeedViewModel() - + let dismiss: () -> Void @State var newLinkURL: String = "" @EnvironmentObject var dataService: DataService - @Environment(\.dismiss) private var dismiss enum FocusField: Hashable { case addLinkEditor @@ -54,7 +21,7 @@ struct LibraryAddFeedView: View { #if os(iOS) Form { innerBody - .navigationTitle("Add Link") + .navigationTitle("Add Feed URL") .navigationBarTitleDisplayMode(.inline) } #else @@ -67,7 +34,6 @@ struct LibraryAddFeedView: View { .onAppear { focusedField = .addLinkEditor } - .navigationTitle("Add Link") #if os(iOS) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -75,14 +41,13 @@ struct LibraryAddFeedView: View { dismissButton } ToolbarItem(placement: .navigationBarTrailing) { - viewModel.isLoading ? AnyView(ProgressView()) : AnyView(addButton) + NavigationLink( + destination: LibraryScanFeedView(dismiss: self.dismiss, viewModel: LibraryAddFeedViewModel(dataService: dataService, feedURL: newLinkURL)), + label: { Text("Add").bold() } + ) } } #endif - .alert(viewModel.errorMessage, - isPresented: $viewModel.showErrorMessage) { - Button(LocalText.genericOk, role: .cancel) { viewModel.showErrorMessage = false } - } } var cancelButton: some View { @@ -102,55 +67,30 @@ struct LibraryAddFeedView: View { var innerBody: some View { Group { - TextField("Add Link", text: $newLinkURL) + TextField("Feed or site URL", text: $newLinkURL) #if os(iOS) .keyboardType(.URL) #endif .autocorrectionDisabled(true) .textFieldStyle(StandardTextFieldStyle()) - .focused($focusedField, equals: .addLinkEditor) +// .focused($focusedField, equals: .addLinkEditor) Button(action: { if let url = pasteboardString { newLinkURL = url } else { - viewModel.error("No URL on pasteboard") + // viewModel.error("No URL on pasteboard") } }, label: { Text("Get from pasteboard") }) - - #if os(macOS) - Spacer() - HStack { - cancelButton - Spacer() - addButton - } - .frame(maxWidth: .infinity) - #endif } } - var addButton: some View { - Button( - action: { - viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss) - }, - label: { Text("Add").bold() } - ) - .keyboardShortcut(.defaultAction) - .onSubmit { - viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss) - } - .disabled(viewModel.isLoading) - } - var dismissButton: some View { Button( action: { dismiss() }, label: { Text(LocalText.genericClose) } ) - .disabled(viewModel.isLoading) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryScanFeedView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryScanFeedView.swift new file mode 100644 index 000000000..9e75076af --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryScanFeedView.swift @@ -0,0 +1,145 @@ +import Foundation +import Services +import SwiftUI +import Utils + +@MainActor +public class LibraryAddFeedViewModel: NSObject, ObservableObject { + let dataService: DataService + let feedURL: String + + @Published var isLoading = true + @Published var errorMessage: String = "" + @Published var showErrorMessage: Bool = false + + @Published var feeds: [Feed] = [] + @Published var selected: [String] = [] + + init(dataService: DataService, feedURL: String) { + self.dataService = dataService + self.feedURL = feedURL + } + + func scanFeed() async { + isLoading = true + if let feedURL = URL(string: feedURL) { + let result = try? await dataService.scanFeed(feedURL: feedURL) + if let feeds = result { + self.feeds = feeds + selected = feeds.map(\.url) + } else { + feeds = [] + error("Error adding feed") + } + } else { + error("invalid URL") + } + isLoading = false + } + + func addFeeds() async { + _ = await withTaskGroup(of: Bool.self) { group in + for feedURL in selected { + group.addTask { + (try? await self.dataService.subscribeToFeed(feedURL: feedURL)) ?? false + } + } + + var successCount = 0 + var failureCount = 0 + for await value in group { + if value { + successCount += 1 + } else { + failureCount += 1 + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) { + if failureCount > 0 { + showInLibrarySnackbar("Failed to add \(failureCount) feeds") + } else { + showInLibrarySnackbar("Added \(successCount) feed\(successCount == 0 ? "" : "s")") + } + } + } + } + + func error(_ msg: String) { + errorMessage = msg + showErrorMessage = true + isLoading = false + } +} + +@MainActor +public struct LibraryScanFeedView: View { + let dismiss: () -> Void + @StateObject var viewModel: LibraryAddFeedViewModel + + func isSelected(_ url: String) -> Bool { + viewModel.selected.contains(url) + } + + var innerBody: some View { + if viewModel.isLoading { + AnyView(ProgressView().frame(maxWidth: .infinity, alignment: .center)) + } else if viewModel.feeds.count == 0 { + AnyView(Text("No feeds found for URL")) + } else { + AnyView(List { + Section("Choose the feeds to add") { + ForEach(viewModel.feeds, id: \.title) { feed in + Button(action: { + if !isSelected(feed.url) { + viewModel.selected.append(feed.url) + } else { + if let idx = viewModel.selected.firstIndex(of: feed.url) { + viewModel.selected.remove(at: idx) + } + } + }, label: { + HStack { + Text(feed.title) + Spacer() + if isSelected(feed.url) { + Image(systemName: "checkmark") + } + } + .contentShape(Rectangle()) + }) + } + } + }) + } + } + + public var body: some View { + Group { + #if os(iOS) + Form { + innerBody + .navigationTitle("Select Feeds") + .navigationBarTitleDisplayMode(.inline) + }.task { + await viewModel.scanFeed() + } + #else + innerBody + #endif + } + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button(action: { + dismiss() + showInLibrarySnackbar("Adding feeds...") + Task { + await viewModel.addFeeds() + } + }, label: { + Text("Done").bold() + }) + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index d7f6117ab..d417292ca 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -6655,6 +6655,7 @@ 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] @@ -6684,6 +6685,10 @@ 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) @@ -6734,6 +6739,7 @@ extension Objects.Filter: Decodable { } } + category = map["category"] createdAt = map["createdAt"] defaultFilter = map["defaultFilter"] description = map["description"] @@ -6748,6 +6754,21 @@ 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): + return data.category[field.alias!] + case .mocking: + return nil + } + } + func createdAt() throws -> DateTime { let field = GraphQLField.leaf( name: "createdAt", @@ -6814,7 +6835,7 @@ extension Fields where TypeLock == Objects.Filter { } } - func folder() throws -> String { + func folder() throws -> String? { let field = GraphQLField.leaf( name: "folder", arguments: [] @@ -6823,12 +6844,9 @@ extension Fields where TypeLock == Objects.Filter { switch response { case let .decoding(data): - if let data = data.folder[field.alias!] { - return data - } - throw HttpError.badpayload + return data.folder[field.alias!] case .mocking: - return String.mockValue + return nil } } @@ -11469,10 +11487,10 @@ extension Fields where TypeLock == Objects.Mutation { } } - func bulkAction(action: Enums.BulkActionType, async: OptionalArgument = .absent(), expectedCount: OptionalArgument = .absent(), labelIds: OptionalArgument<[String]> = .absent(), query: String, selection: Selection) throws -> Type { + func bulkAction(action: Enums.BulkActionType, arguments: OptionalArgument = .absent(), async: OptionalArgument = .absent(), expectedCount: OptionalArgument = .absent(), labelIds: OptionalArgument<[String]> = .absent(), query: String, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "bulkAction", - arguments: [Argument(name: "action", type: "BulkActionType!", value: action), Argument(name: "async", type: "Boolean", value: async), Argument(name: "expectedCount", type: "Int", value: expectedCount), Argument(name: "labelIds", type: "[ID!]", value: labelIds), Argument(name: "query", type: "String!", value: query)], + arguments: [Argument(name: "action", type: "BulkActionType!", value: action), Argument(name: "arguments", type: "JSON", value: arguments), Argument(name: "async", type: "Boolean", value: async), Argument(name: "expectedCount", type: "Int", value: expectedCount), Argument(name: "labelIds", type: "[ID!]", value: labelIds), Argument(name: "query", type: "String!", value: query)], selection: selection.selection ) select(field) @@ -32984,6 +33002,8 @@ extension Enums { case delete = "DELETE" case markAsRead = "MARK_AS_READ" + + case moveToFolder = "MOVE_TO_FOLDER" } } @@ -33682,15 +33702,6 @@ extension Enums { } } -extension Enums { - /// ScanFeedsType - enum ScanFeedsType: String, CaseIterable, Codable { - case html = "HTML" - - case opml = "OPML" - } -} - extension Enums { /// SearchErrorCode enum SearchErrorCode: String, CaseIterable, Codable { @@ -34260,6 +34271,12 @@ extension InputObjects { var preparedDocument: OptionalArgument = .absent() + var publishedAt: OptionalArgument = .absent() + + var rssFeedUrl: OptionalArgument = .absent() + + var savedAt: OptionalArgument = .absent() + var skipParsing: OptionalArgument = .absent() var source: OptionalArgument = .absent() @@ -34276,6 +34293,9 @@ extension InputObjects { 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 publishedAt.hasValue { try container.encode(publishedAt, forKey: .publishedAt) } + if rssFeedUrl.hasValue { try container.encode(rssFeedUrl, forKey: .rssFeedUrl) } + if savedAt.hasValue { try container.encode(savedAt, forKey: .savedAt) } if skipParsing.hasValue { try container.encode(skipParsing, forKey: .skipParsing) } if source.hasValue { try container.encode(source, forKey: .source) } if state.hasValue { try container.encode(state, forKey: .state) } @@ -34288,6 +34308,9 @@ extension InputObjects { case folder case labels case preparedDocument + case publishedAt + case rssFeedUrl + case savedAt case skipParsing case source case state @@ -35031,6 +35054,8 @@ extension InputObjects { extension InputObjects { struct SaveFilterInput: Encodable, Hashable { + var category: OptionalArgument = .absent() + var description: OptionalArgument = .absent() var filter: String @@ -35043,6 +35068,7 @@ 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) } try container.encode(filter, forKey: .filter) if folder.hasValue { try container.encode(folder, forKey: .folder) } @@ -35051,6 +35077,7 @@ extension InputObjects { } enum CodingKeys: String, CodingKey { + case category case description case filter case folder @@ -35174,20 +35201,16 @@ extension InputObjects { struct ScanFeedsInput: Encodable, Hashable { var opml: OptionalArgument = .absent() - var type: Enums.ScanFeedsType - var url: OptionalArgument = .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 } } @@ -35577,6 +35600,8 @@ extension InputObjects { extension InputObjects { struct UpdateFilterInput: Encodable, Hashable { + var category: OptionalArgument = .absent() + var description: OptionalArgument = .absent() var filter: OptionalArgument = .absent() @@ -35593,6 +35618,7 @@ 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) } @@ -35603,6 +35629,7 @@ extension InputObjects { } enum CodingKeys: String, CodingKey { + case category case description case filter case folder diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift deleted file mode 100644 index 8b1378917..000000000 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SubscribeToFeed.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SubscribeToFeed.swift new file mode 100644 index 000000000..4e4520700 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SubscribeToFeed.swift @@ -0,0 +1,46 @@ +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func subscribeToFeed(feedURL: String) async throws -> Bool { + enum MutationResult { + case success(subscriptions: [Subscription]) + case error(errorMessage: String) + } + + let selection = Selection { + try $0.on( + subscribeError: .init { + .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "unknown error") + }, + subscribeSuccess: .init { .success(subscriptions: try $0.subscriptions(selection: subscriptionSelection.list)) } + ) + } + + let mutation = Selection.Mutation { + try $0.subscribe(input: InputObjects.SubscribeInput(url: feedURL), selection: selection) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { mutationResult in + guard let payload = try? mutationResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "failed to add feed \(feedURL)")) + return + } + + switch payload.data { + case .success: + print("subscribed to feed:", feedURL) + continuation.resume(returning: true) + case .error: + print("failed to subscribe to feed:", feedURL) + continuation.resume(throwing: BasicError.message(messageText: "failed to add feed \(feedURL)")) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ScanFeed.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ScanFeed.swift new file mode 100644 index 000000000..52747241c --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ScanFeed.swift @@ -0,0 +1,69 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL + +public struct Feed { + public var title: String + public var url: String + public var description: String? + public var image: String? + public var author: String? +} + +let feedSelection = Selection.Feed { + Feed( + title: try $0.title(), + url: try $0.url(), + description: try $0.description(), + image: try $0.image(), + author: try $0.author() + ) +} + +public extension DataService { + func scanFeed(feedURL: URL) async throws -> [Feed] { + enum QueryResult { + case success(result: [Feed]) + case error(error: String) + } + + let selection = Selection { + try $0.on( + scanFeedsError: .init { + QueryResult.error(error: try $0.errorCodes().description) + }, + scanFeedsSuccess: .init { + QueryResult.success(result: try $0.feeds(selection: feedSelection.list)) + } + ) + } + + let query = Selection.Query { + try $0.scanFeeds( + input: InputObjects.ScanFeedsInput(url: OptionalArgument(feedURL.absoluteString)), + selection: selection + ) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { queryResult in + print("QUERY RESULT: ", queryResult) + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network request failed")) + return + } + + switch payload.data { + case let .success(result: feeds): + continuation.resume(returning: feeds) + case .error: + continuation.resume(throwing: BasicError.message(messageText: "scanFeed fetch error")) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift index 17ee2d565..78bb7577b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift @@ -9,29 +9,13 @@ public extension DataService { case error(error: String) } - let subsciptionSelection = Selection.Subscription { - Subscription( - createdAt: try $0.createdAt().value, - description: try $0.description(), - subscriptionID: try $0.id(), - name: try $0.name(), - newsletterEmailAddress: try $0.newsletterEmail(), - status: try SubscriptionStatus.make(from: $0.status()), - unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(), - unsubscribeMailTo: try $0.unsubscribeMailTo(), - updatedAt: try $0.updatedAt()?.value ?? Date(), - url: try $0.url(), - icon: try $0.icon() - ) - } - let selection = Selection { try $0.on( subscriptionsError: .init { QueryResult.error(error: try $0.errorCodes().description) }, subscriptionsSuccess: .init { - QueryResult.success(result: try $0.subscriptions(selection: subsciptionSelection.list)) + QueryResult.success(result: try $0.subscriptions(selection: subscriptionSelection.list)) } ) } @@ -60,16 +44,3 @@ public extension DataService { } } } - -extension SubscriptionStatus { - static func make(from status: Enums.SubscriptionStatus) -> SubscriptionStatus { - switch status { - case .active: - return .active - case .deleted: - return .deleted - case .unsubscribed: - return .unsubscribed - } - } -} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift index 4017d7276..cee6186dd 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift @@ -6,7 +6,7 @@ let filterSelection = Selection.Filter { InternalFilter( id: try $0.id(), name: try $0.name(), - folder: try $0.folder(), + folder: try $0.folder() ?? "inbox", filter: try $0.filter(), visible: try $0.visible() ?? true, position: try $0.position(), diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/SubsciptionSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/SubsciptionSelection.swift new file mode 100644 index 000000000..32c2d123a --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/SubsciptionSelection.swift @@ -0,0 +1,33 @@ + +import Foundation +import Models +import SwiftGraphQL + +let subscriptionSelection = Selection.Subscription { + Subscription( + createdAt: try $0.createdAt().value, + description: try $0.description(), + subscriptionID: try $0.id(), + name: try $0.name(), + newsletterEmailAddress: try $0.newsletterEmail(), + status: try SubscriptionStatus.make(from: $0.status()), + unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(), + unsubscribeMailTo: try $0.unsubscribeMailTo(), + updatedAt: try $0.updatedAt()?.value ?? Date(), + url: try $0.url(), + icon: try $0.icon() + ) +} + +extension SubscriptionStatus { + static func make(from status: Enums.SubscriptionStatus) -> SubscriptionStatus { + switch status { + case .active: + return .active + case .deleted: + return .deleted + case .unsubscribed: + return .unsubscribed + } + } +} diff --git a/packages/api/package.json b/packages/api/package.json index 34d9b24fb..fdb74b64e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -39,7 +39,7 @@ "@opentelemetry/tracing": "^0.24.0", "@sendgrid/mail": "^7.6.0", "@sentry/integrations": "^7.10.0", - "@sentry/node": "^5.26.0", + "@sentry/node": "^7.9.0", "@sentry/tracing": "^7.9.0", "addressparser": "^1.0.1", "analytics-node": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 891ce9530..a3b0282b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5858,6 +5858,15 @@ "@sentry/types" "7.77.0" "@sentry/utils" "7.77.0" +"@sentry-internal/tracing@7.86.0": + version "7.86.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.86.0.tgz#657e80eb7d08d1030393902c1a7bc47fc39ccb2d" + integrity sha512-b4dUsNWlPWRwakGwR7bhOkqiFlqQszH1hhVFwrm/8s3kqEBZ+E4CeIfCvuHBHQ1cM/fx55xpXX/BU163cy+3iQ== + dependencies: + "@sentry/core" "7.86.0" + "@sentry/types" "7.86.0" + "@sentry/utils" "7.86.0" + "@sentry/browser@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.50.0.tgz#16c995c336322c8aec65570f90f50288678004ec" @@ -5882,17 +5891,6 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" - integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - "@sentry/core@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.50.0.tgz#88bc9cbfc0cb429a28489ece6f0be7a7006436c4" @@ -5910,14 +5908,13 @@ "@sentry/types" "7.77.0" "@sentry/utils" "7.77.0" -"@sentry/hub@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" - integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== +"@sentry/core@7.86.0": + version "7.86.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.86.0.tgz#d01f538783dee9a0d79141a63145392ad2c1cb89" + integrity sha512-SbLvqd1bRYzhDS42u7GMnmbDMfth/zRiLElQWbLK/shmuZzTcfQSwNNdF4Yj+VfjOkqPFgGmICHSHVUc9dh01g== dependencies: - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" + "@sentry/types" "7.86.0" + "@sentry/utils" "7.86.0" "@sentry/integrations@7.50.0": version "7.50.0" @@ -5939,15 +5936,6 @@ "@sentry/utils" "7.77.0" localforage "^1.8.1" -"@sentry/minimal@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" - integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/types" "5.30.0" - tslib "^1.9.3" - "@sentry/nextjs@^7.42.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.50.0.tgz#b9e7727c8f974644bb84a01f40a0adedb44ec416" @@ -5991,20 +5979,16 @@ "@sentry/utils" "7.77.0" https-proxy-agent "^5.0.0" -"@sentry/node@^5.26.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48" - integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg== +"@sentry/node@^7.9.0": + version "7.86.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.86.0.tgz#416db178aeb64f7895a23ae1c3d4d65ce51ed50a" + integrity sha512-cB1bn/LMn2Km97Y3hv63xwWxT50/G5ixGuSxTZ3dCQM6VDhmZoCuC5NGT3itVvaRd6upQXRZa5W0Zgyh0HXKig== dependencies: - "@sentry/core" "5.30.0" - "@sentry/hub" "5.30.0" - "@sentry/tracing" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - cookie "^0.4.1" + "@sentry-internal/tracing" "7.86.0" + "@sentry/core" "7.86.0" + "@sentry/types" "7.86.0" + "@sentry/utils" "7.86.0" https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" "@sentry/react@7.50.0": version "7.50.0" @@ -6038,17 +6022,6 @@ "@types/aws-lambda" "^8.10.62" "@types/express" "^4.17.14" -"@sentry/tracing@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" - integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - "@sentry/tracing@^7.9.0": version "7.77.0" resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.77.0.tgz#39d7c30834f503fe9eb20ce1c8c8bd28f7d7c9ce" @@ -6056,11 +6029,6 @@ dependencies: "@sentry-internal/tracing" "7.77.0" -"@sentry/types@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" - integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== - "@sentry/types@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.50.0.tgz#52a035cad83a80ca26fa53c09eb1241250c3df3e" @@ -6071,13 +6039,10 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.77.0.tgz#c5d00fe547b89ccde59cdea59143bf145cee3144" integrity sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA== -"@sentry/utils@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" - integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== - dependencies: - "@sentry/types" "5.30.0" - tslib "^1.9.3" +"@sentry/types@7.86.0": + version "7.86.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.86.0.tgz#56ed2f5b15e8130ea5ecfbbc4102d88eaa3b3a67" + integrity sha512-pGAt0+bMfWgo0KG2epthfNV4Wae03tURpoxNjGo5Fr4cXxvLTSijSAQ6rmmO4bXBJ7+rErEjX30g30o/eEdP9g== "@sentry/utils@7.50.0": version "7.50.0" @@ -6094,6 +6059,13 @@ dependencies: "@sentry/types" "7.77.0" +"@sentry/utils@7.86.0": + version "7.86.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.86.0.tgz#356ec19bf1e3e5c40935dd987fd15bee8c6b37ba" + integrity sha512-6PejFtw9VTFFy5vu0ks+U7Ozkqz+eMt+HN8AZKBKErYzX5/xs0kpkOcSRpu3ETdTYcZf8VAmLVgFgE2BE+3WuQ== + dependencies: + "@sentry/types" "7.86.0" + "@sentry/webpack-plugin@1.20.0": version "1.20.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58"