mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #138 from omnivore-app/feature/emails-ui
Newsletter Email for mac/iOS
This commit is contained in:
commit
de07e9ad92
11 changed files with 660 additions and 29 deletions
|
|
@ -40,7 +40,7 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
|
|||
case .feed:
|
||||
HomeFeedView()
|
||||
case .profile:
|
||||
ProfileContainerView()
|
||||
ProfileView()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
9
apple/OmnivoreKit/Sources/App/SnackbarExtension.swift
Normal file
9
apple/OmnivoreKit/Sources/App/SnackbarExtension.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import Foundation
|
||||
import Services
|
||||
import Views
|
||||
|
||||
extension Snackbar {
|
||||
static func show(message: String) {
|
||||
NSNotification.operationSuccess(message: message)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ final class HomeFeedViewModel: ObservableObject {
|
|||
receiveValue: { [weak self] _ in
|
||||
self?.isLoading = false
|
||||
stopNetworkActivityIndicator()
|
||||
NSNotification.operationSuccess(message: archived ? "Link archived" : "Link moved to Inbox")
|
||||
Snackbar.show(message: archived ? "Link archived" : "Link moved to Inbox")
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
|
|
@ -130,16 +130,15 @@ final class HomeFeedViewModel: ObservableObject {
|
|||
dataService.removeLinkPublisher(itemID: linkId)
|
||||
.sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case let .failure(error) = completion else { return }
|
||||
guard case .failure = completion else { return }
|
||||
self?.isLoading = false
|
||||
stopNetworkActivityIndicator()
|
||||
print(error)
|
||||
NSNotification.operationFailed(message: "Failed to remove link")
|
||||
Snackbar.show(message: "Failed to remove link")
|
||||
},
|
||||
receiveValue: { [weak self] _ in
|
||||
self?.isLoading = false
|
||||
stopNetworkActivityIndicator()
|
||||
NSNotification.operationSuccess(message: "Link removed")
|
||||
Snackbar.show(message: "Link removed")
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
|
|
@ -169,7 +168,7 @@ final class HomeFeedViewModel: ObservableObject {
|
|||
self?.isLoading = false
|
||||
stopNetworkActivityIndicator()
|
||||
if let message = successMessage {
|
||||
NSNotification.operationSuccess(message: message)
|
||||
Snackbar.show(message: message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -391,7 +390,7 @@ struct HomeFeedView: View {
|
|||
.toolbar {
|
||||
ToolbarItem {
|
||||
NavigationLink(
|
||||
destination: { ProfileContainerView() },
|
||||
destination: { ProfileView() },
|
||||
label: {
|
||||
Image.profile
|
||||
.resizable()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Views
|
||||
|
||||
final class NewsletterEmailsViewModel: ObservableObject {
|
||||
private var hasLoadedInitialEmails = false
|
||||
@Published var isLoading = false
|
||||
@Published var emails = [NewsletterEmail]()
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
func loadEmails(dataService: DataService) {
|
||||
isLoading = true
|
||||
|
||||
dataService.newsletterEmailsPublisher().sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { [weak self] result in
|
||||
self?.isLoading = false
|
||||
self?.emails = result
|
||||
self?.hasLoadedInitialEmails = true
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func createEmail(dataService: DataService) {
|
||||
isLoading = true
|
||||
|
||||
dataService.createNewsletterEmailPublisher().sink(
|
||||
receiveCompletion: { [weak self] _ in
|
||||
self?.isLoading = false
|
||||
},
|
||||
receiveValue: { [weak self] result in
|
||||
self?.isLoading = false
|
||||
self?.emails.insert(result, at: 0)
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
struct NewsletterEmailsView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@ObservedObject var viewModel = NewsletterEmailsViewModel()
|
||||
let footerText = "Add PDFs to your library, or subscribe to emails using an Omnivore email address."
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Form {
|
||||
innerBody
|
||||
}
|
||||
#elseif os(macOS)
|
||||
List {
|
||||
innerBody
|
||||
}
|
||||
.listStyle(InsetListStyle())
|
||||
#endif
|
||||
}
|
||||
.onAppear { viewModel.loadEmails(dataService: dataService) }
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
Group {
|
||||
Section(footer: Text(footerText)) {
|
||||
Button(
|
||||
action: {
|
||||
viewModel.createEmail(dataService: dataService)
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill").foregroundColor(.green)
|
||||
Text("Create a new email address")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
|
||||
if !viewModel.emails.isEmpty {
|
||||
Section(header: Text("Existing Emails (Tap to copy)")) {
|
||||
ForEach(viewModel.emails) { newsletterEmail in
|
||||
Button(
|
||||
action: {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = newsletterEmail.email
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
let pasteBoard = NSPasteboard.general
|
||||
pasteBoard.clearContents()
|
||||
pasteBoard.writeObjects([newsletterEmail.email as NSString])
|
||||
#endif
|
||||
|
||||
Snackbar.show(message: "Email copied")
|
||||
},
|
||||
label: { Text(newsletterEmail.email) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Emails")
|
||||
}
|
||||
}
|
||||
|
|
@ -26,11 +26,12 @@ final class ProfileContainerViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
struct ProfileContainerView: View {
|
||||
struct ProfileView: View {
|
||||
@EnvironmentObject var authenticator: Authenticator
|
||||
@EnvironmentObject var dataService: DataService
|
||||
|
||||
@ObservedObject private var viewModel = ProfileContainerViewModel()
|
||||
|
||||
@State private var showLogoutConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -50,9 +51,13 @@ struct ProfileContainerView: View {
|
|||
Group {
|
||||
Section {
|
||||
ProfileCard(data: viewModel.profileCardData)
|
||||
.onAppear {
|
||||
viewModel.loadProfileData(dataService: dataService)
|
||||
}
|
||||
.onAppear { viewModel.loadProfileData(dataService: dataService) }
|
||||
}
|
||||
|
||||
Section {
|
||||
NavigationLink(destination: NewsletterEmailsView()) {
|
||||
Text("Emails")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
|
|
@ -70,9 +75,7 @@ struct ProfileContainerView: View {
|
|||
|
||||
#if os(iOS)
|
||||
Button(
|
||||
action: {
|
||||
DataService.showIntercomMessenger?()
|
||||
},
|
||||
action: { DataService.showIntercomMessenger?() },
|
||||
label: { Text("Feedback") }
|
||||
)
|
||||
#endif
|
||||
|
|
@ -118,12 +121,12 @@ private extension BasicWebAppView {
|
|||
}
|
||||
|
||||
private static func omnivoreWebView(path: String, baseURL: URL) -> BasicWebAppView {
|
||||
let urlRequest = URLRequest.webRequest(
|
||||
baseURL: baseURL,
|
||||
urlPath: path,
|
||||
queryParams: nil
|
||||
)
|
||||
let url: URL = {
|
||||
var urlComponents = URLComponents()
|
||||
urlComponents.path = path
|
||||
return urlComponents.url(relativeTo: baseURL)!
|
||||
}()
|
||||
|
||||
return BasicWebAppView(request: urlRequest)
|
||||
return BasicWebAppView(request: URLRequest(url: url))
|
||||
}
|
||||
}
|
||||
14
apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift
Normal file
14
apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import Foundation
|
||||
|
||||
public struct NewsletterEmail: Identifiable {
|
||||
public let id = UUID()
|
||||
public let emailId: String
|
||||
public let email: String
|
||||
public let confirmationCode: String?
|
||||
|
||||
public init(emailId: String, email: String, confirmationCode: String?) {
|
||||
self.emailId = emailId
|
||||
self.email = email
|
||||
self.confirmationCode = confirmationCode
|
||||
}
|
||||
}
|
||||
|
|
@ -2957,6 +2957,8 @@ extension Objects {
|
|||
let id: [String: String]
|
||||
let image: [String: String]
|
||||
let isArchived: [String: Bool]
|
||||
let labels: [String: [Objects.Label]]
|
||||
let linkId: [String: String]
|
||||
let originalArticleUrl: [String: String]
|
||||
let originalHtml: [String: String]
|
||||
let pageType: [String: Enums.PageType]
|
||||
|
|
@ -3034,6 +3036,14 @@ extension Objects.Article: Decodable {
|
|||
if let value = try container.decode(Bool?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "labels":
|
||||
if let value = try container.decode([Objects.Label]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "linkId":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "originalArticleUrl":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -3111,6 +3121,8 @@ extension Objects.Article: Decodable {
|
|||
id = map["id"]
|
||||
image = map["image"]
|
||||
isArchived = map["isArchived"]
|
||||
labels = map["labels"]
|
||||
linkId = map["linkId"]
|
||||
originalArticleUrl = map["originalArticleUrl"]
|
||||
originalHtml = map["originalHtml"]
|
||||
pageType = map["pageType"]
|
||||
|
|
@ -3544,6 +3556,37 @@ extension Fields where TypeLock == Objects.Article {
|
|||
return Bool.mockValue
|
||||
}
|
||||
}
|
||||
|
||||
func linkId() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "linkId",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.linkId[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func labels<Type>(selection: Selection<Type, [Objects.Label]?>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "labels",
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return try selection.decode(data: data.labels[field.alias!])
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
|
|
@ -10609,6 +10652,9 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
extension Objects {
|
||||
struct Label {
|
||||
let __typename: TypeName = .label
|
||||
let color: [String: String]
|
||||
let createdAt: [String: DateTime]
|
||||
let description: [String: String]
|
||||
let id: [String: String]
|
||||
let name: [String: String]
|
||||
|
||||
|
|
@ -10630,6 +10676,18 @@ extension Objects.Label: Decodable {
|
|||
let field = GraphQLField.getFieldNameFromAlias(alias)
|
||||
|
||||
switch field {
|
||||
case "color":
|
||||
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)
|
||||
}
|
||||
case "description":
|
||||
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)
|
||||
|
|
@ -10648,6 +10706,9 @@ extension Objects.Label: Decodable {
|
|||
}
|
||||
}
|
||||
|
||||
color = map["color"]
|
||||
createdAt = map["createdAt"]
|
||||
description = map["description"]
|
||||
id = map["id"]
|
||||
name = map["name"]
|
||||
}
|
||||
|
|
@ -10689,6 +10750,57 @@ extension Fields where TypeLock == Objects.Label {
|
|||
return String.mockValue
|
||||
}
|
||||
}
|
||||
|
||||
func color() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "color",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.color[field.alias!] {
|
||||
return data
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return String.mockValue
|
||||
}
|
||||
}
|
||||
|
||||
func description() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "description",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.description[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createdAt() throws -> DateTime {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "createdAt",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.createdAt[field.alias!] {
|
||||
return data
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return DateTime.mockValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
|
|
@ -11219,6 +11331,137 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
typealias SignupError<T> = Selection<T, Objects.SignupError>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct SetLabelsSuccess {
|
||||
let __typename: TypeName = .setLabelsSuccess
|
||||
let labels: [String: [Objects.Label]]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case setLabelsSuccess = "SetLabelsSuccess"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Objects.SetLabelsSuccess: 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 "labels":
|
||||
if let value = try container.decode([Objects.Label]?.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)."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
labels = map["labels"]
|
||||
}
|
||||
}
|
||||
|
||||
extension Fields where TypeLock == Objects.SetLabelsSuccess {
|
||||
func labels<Type>(selection: Selection<Type, [Objects.Label]>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "labels",
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.labels[field.alias!] {
|
||||
return try selection.decode(data: data)
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias SetLabelsSuccess<T> = Selection<T, Objects.SetLabelsSuccess>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct SetLabelsError {
|
||||
let __typename: TypeName = .setLabelsError
|
||||
let errorCodes: [String: [Enums.SetLabelsErrorCode]]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case setLabelsError = "SetLabelsError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Objects.SetLabelsError: 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.SetLabelsErrorCode]?.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.SetLabelsError {
|
||||
func errorCodes() throws -> [Enums.SetLabelsErrorCode] {
|
||||
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 SetLabelsError<T> = Selection<T, Objects.SetLabelsError>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct Mutation {
|
||||
let __typename: TypeName = .mutation
|
||||
|
|
@ -11249,6 +11492,7 @@ extension Objects {
|
|||
let setBookmarkArticle: [String: Unions.SetBookmarkArticleResult]
|
||||
let setDeviceToken: [String: Unions.SetDeviceTokenResult]
|
||||
let setFollow: [String: Unions.SetFollowResult]
|
||||
let setLabels: [String: Unions.SetLabelsResult]
|
||||
let setLinkArchived: [String: Unions.ArchiveLinkResult]
|
||||
let setShareArticle: [String: Unions.SetShareArticleResult]
|
||||
let setShareHighlight: [String: Unions.SetShareHighlightResult]
|
||||
|
|
@ -11389,6 +11633,10 @@ extension Objects.Mutation: Decodable {
|
|||
if let value = try container.decode(Unions.SetFollowResult?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "setLabels":
|
||||
if let value = try container.decode(Unions.SetLabelsResult?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "setLinkArchived":
|
||||
if let value = try container.decode(Unions.ArchiveLinkResult?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -11478,6 +11726,7 @@ extension Objects.Mutation: Decodable {
|
|||
setBookmarkArticle = map["setBookmarkArticle"]
|
||||
setDeviceToken = map["setDeviceToken"]
|
||||
setFollow = map["setFollow"]
|
||||
setLabels = map["setLabels"]
|
||||
setLinkArchived = map["setLinkArchived"]
|
||||
setShareArticle = map["setShareArticle"]
|
||||
setShareHighlight = map["setShareHighlight"]
|
||||
|
|
@ -12254,6 +12503,25 @@ extension Fields where TypeLock == Objects.Mutation {
|
|||
return selection.mock()
|
||||
}
|
||||
}
|
||||
|
||||
func setLabels<Type>(input: InputObjects.SetLabelsInput, selection: Selection<Type, Unions.SetLabelsResult>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "setLabels",
|
||||
arguments: [Argument(name: "input", type: "SetLabelsInput!", value: input)],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.setLabels[field.alias!] {
|
||||
return try selection.decode(data: data)
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
|
|
@ -12669,10 +12937,10 @@ extension Fields where TypeLock == Objects.Query {
|
|||
}
|
||||
}
|
||||
|
||||
func labels<Type>(linkId: String, selection: Selection<Type, Unions.LabelsResult>) throws -> Type {
|
||||
func labels<Type>(selection: Selection<Type, Unions.LabelsResult>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "labels",
|
||||
arguments: [Argument(name: "linkId", type: "ID!", value: linkId)],
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
|
@ -16404,6 +16672,80 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
typealias SignupResult<T> = Selection<T, Unions.SignupResult>
|
||||
}
|
||||
|
||||
extension Unions {
|
||||
struct SetLabelsResult {
|
||||
let __typename: TypeName
|
||||
let errorCodes: [String: [Enums.SetLabelsErrorCode]]
|
||||
let labels: [String: [Objects.Label]]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case setLabelsSuccess = "SetLabelsSuccess"
|
||||
case setLabelsError = "SetLabelsError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Unions.SetLabelsResult: 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.SetLabelsErrorCode]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "labels":
|
||||
if let value = try container.decode([Objects.Label]?.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"]
|
||||
labels = map["labels"]
|
||||
}
|
||||
}
|
||||
|
||||
extension Fields where TypeLock == Unions.SetLabelsResult {
|
||||
func on<Type>(setLabelsSuccess: Selection<Type, Objects.SetLabelsSuccess>, setLabelsError: Selection<Type, Objects.SetLabelsError>) throws -> Type {
|
||||
select([GraphQLField.fragment(type: "SetLabelsSuccess", selection: setLabelsSuccess.selection), GraphQLField.fragment(type: "SetLabelsError", selection: setLabelsError.selection)])
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
switch data.__typename {
|
||||
case .setLabelsSuccess:
|
||||
let data = Objects.SetLabelsSuccess(labels: data.labels)
|
||||
return try setLabelsSuccess.decode(data: data)
|
||||
case .setLabelsError:
|
||||
let data = Objects.SetLabelsError(errorCodes: data.errorCodes)
|
||||
return try setLabelsError.decode(data: data)
|
||||
}
|
||||
case .mocking:
|
||||
return setLabelsSuccess.mock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias SetLabelsResult<T> = Selection<T, Unions.SetLabelsResult>
|
||||
}
|
||||
|
||||
// MARK: - Enums
|
||||
|
||||
enum Enums {}
|
||||
|
|
@ -16997,6 +17339,8 @@ extension Enums {
|
|||
case badRequest = "BAD_REQUEST"
|
||||
|
||||
case notFound = "NOT_FOUND"
|
||||
|
||||
case labelAlreadyExists = "LABEL_ALREADY_EXISTS"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -17011,6 +17355,17 @@ extension Enums {
|
|||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// SetLabelsErrorCode
|
||||
enum SetLabelsErrorCode: String, CaseIterable, Codable {
|
||||
case unauthorized = "UNAUTHORIZED"
|
||||
|
||||
case badRequest = "BAD_REQUEST"
|
||||
|
||||
case notFound = "NOT_FOUND"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Input Objects
|
||||
|
||||
enum InputObjects {}
|
||||
|
|
@ -17851,19 +18206,23 @@ extension InputObjects {
|
|||
|
||||
extension InputObjects {
|
||||
struct CreateLabelInput: Encodable, Hashable {
|
||||
var linkId: String
|
||||
|
||||
var name: String
|
||||
|
||||
var color: String
|
||||
|
||||
var description: OptionalArgument<String> = .absent()
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(linkId, forKey: .linkId)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(color, forKey: .color)
|
||||
if description.hasValue { try container.encode(description, forKey: .description) }
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case linkId
|
||||
case name
|
||||
case color
|
||||
case description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17921,3 +18280,22 @@ extension InputObjects {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InputObjects {
|
||||
struct SetLabelsInput: Encodable, Hashable {
|
||||
var linkId: String
|
||||
|
||||
var labelIds: [String]
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(linkId, forKey: .linkId)
|
||||
try container.encode(labelIds, forKey: .labelIds)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case linkId
|
||||
case labelIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func createNewsletterEmailPublisher() -> AnyPublisher<NewsletterEmail, BasicError> {
|
||||
enum MutationResult {
|
||||
case saved(newsletterEmail: NewsletterEmail)
|
||||
case error(errorCode: Enums.CreateNewsletterEmailErrorCode)
|
||||
}
|
||||
|
||||
let selection = Selection<MutationResult, Unions.CreateNewsletterEmailResult> {
|
||||
try $0.on(
|
||||
createNewsletterEmailSuccess: .init {
|
||||
.saved(newsletterEmail: try $0.newsletterEmail(selection: Selection.NewsletterEmail {
|
||||
NewsletterEmail(
|
||||
emailId: try $0.id(),
|
||||
email: try $0.address(),
|
||||
confirmationCode: try $0.confirmationCode()
|
||||
)
|
||||
}))
|
||||
}, createNewsletterEmailError: .init {
|
||||
.error(errorCode: try $0.errorCodes().first ?? .badRequest)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.createNewsletterEmail(selection: selection)
|
||||
}
|
||||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
send(mutation, to: path, headers: headers) { result in
|
||||
switch result {
|
||||
case let .success(payload):
|
||||
if let graphqlError = payload.errors {
|
||||
promise(.failure(.message(messageText: "graphql error: \(graphqlError)")))
|
||||
}
|
||||
|
||||
switch payload.data {
|
||||
case let .saved(newsletterEmail: newsletterEmail):
|
||||
promise(.success(newsletterEmail))
|
||||
case let .error(errorCode: errorCode):
|
||||
promise(.failure(.message(messageText: errorCode.rawValue)))
|
||||
}
|
||||
case .failure:
|
||||
promise(.failure(.message(messageText: "graphql error")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func newsletterEmailsPublisher() -> AnyPublisher<[NewsletterEmail], ServerError> {
|
||||
enum QueryResult {
|
||||
case success(result: [NewsletterEmail])
|
||||
case error(error: String)
|
||||
}
|
||||
|
||||
let newsletterEmailSelection = Selection.NewsletterEmail {
|
||||
NewsletterEmail(
|
||||
emailId: try $0.id(),
|
||||
email: try $0.address(),
|
||||
confirmationCode: try $0.confirmationCode()
|
||||
)
|
||||
}
|
||||
|
||||
let selection = Selection<QueryResult, Unions.NewsletterEmailsResult> {
|
||||
try $0.on(
|
||||
newsletterEmailsSuccess: .init {
|
||||
QueryResult.success(result: try $0.newsletterEmails(selection: newsletterEmailSelection.list))
|
||||
},
|
||||
newsletterEmailsError: .init {
|
||||
QueryResult.error(error: try $0.errorCodes().description)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let query = Selection.Query {
|
||||
try $0.newsletterEmails(selection: selection)
|
||||
}
|
||||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
send(query, to: path, headers: headers) { result in
|
||||
switch result {
|
||||
case let .success(payload):
|
||||
switch payload.data {
|
||||
case let .success(result: result):
|
||||
promise(.success(result))
|
||||
case .error:
|
||||
promise(.failure(.unknown))
|
||||
}
|
||||
case .failure:
|
||||
promise(.failure(.unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import SwiftUI
|
||||
|
||||
struct Snackbar: View {
|
||||
public struct Snackbar: View {
|
||||
@Binding var isShowing: Bool
|
||||
private let presenting: AnyView
|
||||
private let text: Text
|
||||
|
|
@ -27,7 +27,7 @@ struct Snackbar: View {
|
|||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
public var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .center) {
|
||||
self.presenting
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@ GCS_UPLOAD_BUCKET=
|
|||
GCS_UPLOAD_SA_KEY_FILE_PATH=
|
||||
TWITTER_BEARER_TOKEN=
|
||||
PREVIEW_IMAGE_WRAPPER_ID='selected_highlight_wrapper'
|
||||
REMINDER_TASK_HANDLER_URL=
|
||||
Loading…
Reference in a new issue