Add feed buttons for iOS

This commit is contained in:
Jackson Harper 2023-12-11 17:36:48 +08:00
parent f47c83d3eb
commit 6764f56cdd
14 changed files with 448 additions and 233 deletions

View file

@ -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 {

View file

@ -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<String>
@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.",

View file

@ -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?

View file

@ -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)
}
}

View file

@ -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()
})
}
}
}
}

View file

@ -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<Type>(action: Enums.BulkActionType, async: OptionalArgument<Bool> = .absent(), expectedCount: OptionalArgument<Int> = .absent(), labelIds: OptionalArgument<[String]> = .absent(), query: String, selection: Selection<Type, Unions.BulkActionResult>) throws -> Type {
func bulkAction<Type>(action: Enums.BulkActionType, arguments: OptionalArgument<String> = .absent(), async: OptionalArgument<Bool> = .absent(), expectedCount: OptionalArgument<Int> = .absent(), labelIds: OptionalArgument<[String]> = .absent(), query: String, selection: Selection<Type, Unions.BulkActionResult>) 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<InputObjects.PreparedDocumentInput> = .absent()
var publishedAt: OptionalArgument<DateTime> = .absent()
var rssFeedUrl: OptionalArgument<String> = .absent()
var savedAt: OptionalArgument<DateTime> = .absent()
var skipParsing: OptionalArgument<Bool> = .absent()
var source: OptionalArgument<String> = .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<String> = .absent()
var description: OptionalArgument<String> = .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<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
}
}
@ -35577,6 +35600,8 @@ extension InputObjects {
extension InputObjects {
struct UpdateFilterInput: Encodable, Hashable {
var category: OptionalArgument<String> = .absent()
var description: OptionalArgument<String> = .absent()
var filter: OptionalArgument<String> = .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

View file

@ -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<MutationResult, Unions.SubscribeResult> {
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)"))
}
}
}
}
}

View file

@ -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<QueryResult, Unions.ScanFeedsResult> {
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"))
}
}
}
}
}

View file

@ -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<QueryResult, Unions.SubscriptionsResult> {
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
}
}
}

View file

@ -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(),

View file

@ -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
}
}
}

View file

@ -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",

View file

@ -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"