From 8538a48b37516ab72d5d7090c667cb6c462d84e1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 24 Feb 2022 21:51:20 -0800 Subject: [PATCH 01/13] create profile child view --- .../App/Views/ProfileContainerView.swift | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift index 5832b0b63..980de2a2b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift @@ -31,8 +31,25 @@ struct ProfileContainerView: View { @EnvironmentObject var dataService: DataService @ObservedObject private var viewModel = ProfileContainerViewModel() + + var body: some View { + ProfileView( + profileCardData: viewModel.profileCardData, + webAppBaseURL: dataService.appEnvironment.webAppBaseURL, + onAppearAction: { viewModel.loadProfileData(dataService: dataService) }, + logoutAction: authenticator.logout + ) + } +} + +struct ProfileView: View { @State private var showLogoutConfirmation = false + let profileCardData: ProfileCardData + let webAppBaseURL: URL + let onAppearAction: () -> Void + let logoutAction: () -> Void + var body: some View { #if os(iOS) Form { @@ -49,21 +66,19 @@ struct ProfileContainerView: View { private var innerBody: some View { Group { Section { - ProfileCard(data: viewModel.profileCardData) - .onAppear { - viewModel.loadProfileData(dataService: dataService) - } + ProfileCard(data: profileCardData) + .onAppear { onAppearAction() } } Section { NavigationLink( - destination: BasicWebAppView.privacyPolicyWebView(baseURL: dataService.appEnvironment.webAppBaseURL) + destination: BasicWebAppView.privacyPolicyWebView(baseURL: webAppBaseURL) ) { Text("Privacy Policy") } NavigationLink( - destination: BasicWebAppView.termsConditionsWebView(baseURL: dataService.appEnvironment.webAppBaseURL) + destination: BasicWebAppView.termsConditionsWebView(baseURL: webAppBaseURL) ) { Text("Terms and Conditions") } @@ -97,7 +112,7 @@ struct ProfileContainerView: View { Alert( title: Text("Are you sure you want to logout?"), primaryButton: .destructive(Text("Confirm")) { - authenticator.logout() + logoutAction() }, secondaryButton: .cancel() ) From 7babb90d7aef870d977a199949c7e903be44c90e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 24 Feb 2022 22:22:04 -0800 Subject: [PATCH 02/13] use action enum for profile view --- .../App/Views/ProfileContainerView.swift | 115 ++--------------- .../Views/UserSettings/ProfileView.swift | 119 ++++++++++++++++++ 2 files changed, 131 insertions(+), 103 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Views/UserSettings/ProfileView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift index 980de2a2b..d69c4735d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift @@ -32,113 +32,22 @@ struct ProfileContainerView: View { @ObservedObject private var viewModel = ProfileContainerViewModel() + func actionHandler(action: ProfileViewAction) { + switch action { + case .loadProfileAction: + viewModel.loadProfileData(dataService: dataService) + case .logout: + authenticator.logout() + case .showIntercomMessenger: + DataService.showIntercomMessenger?() + } + } + var body: some View { ProfileView( profileCardData: viewModel.profileCardData, webAppBaseURL: dataService.appEnvironment.webAppBaseURL, - onAppearAction: { viewModel.loadProfileData(dataService: dataService) }, - logoutAction: authenticator.logout + actionHandler: actionHandler ) } } - -struct ProfileView: View { - @State private var showLogoutConfirmation = false - - let profileCardData: ProfileCardData - let webAppBaseURL: URL - let onAppearAction: () -> Void - let logoutAction: () -> Void - - var body: some View { - #if os(iOS) - Form { - innerBody - } - #elseif os(macOS) - List { - innerBody - } - .listStyle(InsetListStyle()) - #endif - } - - private var innerBody: some View { - Group { - Section { - ProfileCard(data: profileCardData) - .onAppear { onAppearAction() } - } - - Section { - NavigationLink( - destination: BasicWebAppView.privacyPolicyWebView(baseURL: webAppBaseURL) - ) { - Text("Privacy Policy") - } - - NavigationLink( - destination: BasicWebAppView.termsConditionsWebView(baseURL: webAppBaseURL) - ) { - Text("Terms and Conditions") - } - - #if os(iOS) - Button( - action: { - DataService.showIntercomMessenger?() - }, - label: { Text("Feedback") } - ) - #endif - } - - Section { - if FeatureFlag.showAccountDeletion { - NavigationLink( - destination: ManageAccountView(handleAccountDeletion: { - print("delete account") - }) - ) { - Text("Manage Account") - } - } - - Text("Logout") - .onTapGesture { - showLogoutConfirmation = true - } - .alert(isPresented: $showLogoutConfirmation) { - Alert( - title: Text("Are you sure you want to logout?"), - primaryButton: .destructive(Text("Confirm")) { - logoutAction() - }, - secondaryButton: .cancel() - ) - } - } - } - .navigationTitle("Profile") - } -} - -private extension BasicWebAppView { - static func privacyPolicyWebView(baseURL: URL) -> BasicWebAppView { - omnivoreWebView(path: "/app/privacy", baseURL: baseURL) - } - - static func termsConditionsWebView(baseURL: URL) -> BasicWebAppView { - omnivoreWebView(path: "/app/terms", baseURL: baseURL) - } - - private static func omnivoreWebView(path: String, baseURL: URL) -> BasicWebAppView { - let urlRequest = URLRequest.webRequest( - baseURL: baseURL, - urlPath: path, - queryParams: nil - ) - - return BasicWebAppView(request: urlRequest) - } -} diff --git a/apple/OmnivoreKit/Sources/Views/UserSettings/ProfileView.swift b/apple/OmnivoreKit/Sources/Views/UserSettings/ProfileView.swift new file mode 100644 index 000000000..55706705e --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/UserSettings/ProfileView.swift @@ -0,0 +1,119 @@ +import Models +import SwiftUI +import Utils + +public enum ProfileViewAction { + case loadProfileAction + case logout + case showIntercomMessenger +} + +public struct ProfileView: View { + @State private var showLogoutConfirmation = false + + let profileCardData: ProfileCardData + let webAppBaseURL: URL + let actionHandler: (ProfileViewAction) -> Void + + public init( + profileCardData: ProfileCardData, + webAppBaseURL: URL, + actionHandler: @escaping (ProfileViewAction) -> Void + ) { + self.profileCardData = profileCardData + self.webAppBaseURL = webAppBaseURL + self.actionHandler = actionHandler + } + + public var body: some View { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } + + private var innerBody: some View { + Group { + Section { + ProfileCard(data: profileCardData) + .onAppear { actionHandler(.loadProfileAction) } + } + + Section { + NavigationLink( + destination: BasicWebAppView.privacyPolicyWebView(baseURL: webAppBaseURL) + ) { + Text("Privacy Policy") + } + + NavigationLink( + destination: BasicWebAppView.termsConditionsWebView(baseURL: webAppBaseURL) + ) { + Text("Terms and Conditions") + } + + #if os(iOS) + Button( + action: { + actionHandler(.showIntercomMessenger) + }, + label: { Text("Feedback") } + ) + #endif + } + + Section { + if FeatureFlag.showAccountDeletion { + NavigationLink( + destination: ManageAccountView(handleAccountDeletion: { + print("delete account") + }) + ) { + Text("Manage Account") + } + } + + Text("Logout") + .onTapGesture { + showLogoutConfirmation = true + } + .alert(isPresented: $showLogoutConfirmation) { + Alert( + title: Text("Are you sure you want to logout?"), + primaryButton: .destructive(Text("Confirm")) { + actionHandler(.logout) + }, + secondaryButton: .cancel() + ) + } + } + } + .navigationTitle("Profile") + } +} + +private extension BasicWebAppView { + static func privacyPolicyWebView(baseURL: URL) -> BasicWebAppView { + omnivoreWebView(path: "/app/privacy", baseURL: baseURL) + } + + static func termsConditionsWebView(baseURL: URL) -> BasicWebAppView { + omnivoreWebView(path: "/app/terms", baseURL: baseURL) + } + + private static func omnivoreWebView(path: String, baseURL: URL) -> BasicWebAppView { + let url: URL = { + var urlComponents = URLComponents() + urlComponents.path = path + return urlComponents.url(relativeTo: baseURL)! + }() + + return BasicWebAppView(request: URLRequest(url: url)) + } +} From bf4e91ad52cd2ce76e9684b0d8e8543198959cac Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 09:57:00 -0800 Subject: [PATCH 03/13] create omnivore email relay ui for apple apps --- .../Sources/App/PrimaryContentCategory.swift | 2 +- .../Sources/App/Views/HomeFeedView.swift | 2 +- .../App/Views/Profile/EmailsView.swift | 53 ++++++++++++++ .../Views/Profile}/ProfileView.swift | 69 +++++++++++-------- .../App/Views/ProfileContainerView.swift | 53 -------------- 5 files changed, 96 insertions(+), 83 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift rename apple/OmnivoreKit/Sources/{Views/UserSettings => App/Views/Profile}/ProfileView.swift (61%) delete mode 100644 apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift diff --git a/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift b/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift index 60ddf6863..699c63cb9 100644 --- a/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift +++ b/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift @@ -40,7 +40,7 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable { case .feed: HomeFeedView() case .profile: - ProfileContainerView() + ProfileView() } } diff --git a/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift b/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift index f87f7eadc..7f5d2fc46 100644 --- a/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift @@ -391,7 +391,7 @@ struct HomeFeedView: View { .toolbar { ToolbarItem { NavigationLink( - destination: { ProfileContainerView() }, + destination: { ProfileView() }, label: { Image.profile .resizable() diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift new file mode 100644 index 000000000..4036e1706 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift @@ -0,0 +1,53 @@ +import SwiftUI + +struct EmailsView: View { + let footerText = "Add PDFs to your library, or subscribe to emails using an Omnivore email address." + + @State var emails = [String]() + + var body: some View { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } + + private var innerBody: some View { + Group { + Section(footer: Text(footerText)) { + Button( + action: { + withAnimation { + emails.insert("newemail@omnivore-relay.app\(emails.count)", at: 0) + } + }, + label: { + HStack { + Image(systemName: "plus.circle.fill").foregroundColor(.green) + Text("Create a new email address") + Spacer() + } + } + ) + } + + if !emails.isEmpty { + Section(header: Text("Existing Emails (Tap to copy)")) { + ForEach(emails, id: \.self) { email in + Button( + action: {}, + label: { Text(email) } + ) + } + } + } + } + .navigationTitle("Emails") + } +} diff --git a/apple/OmnivoreKit/Sources/Views/UserSettings/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift similarity index 61% rename from apple/OmnivoreKit/Sources/Views/UserSettings/ProfileView.swift rename to apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 55706705e..b81554cb9 100644 --- a/apple/OmnivoreKit/Sources/Views/UserSettings/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -1,31 +1,40 @@ +import Combine import Models +import Services import SwiftUI import Utils +import Views -public enum ProfileViewAction { - case loadProfileAction - case logout - case showIntercomMessenger +final class ProfileContainerViewModel: ObservableObject { + @Published var isLoading = false + @Published var profileCardData = ProfileCardData() + + var subscriptions = Set() + + func loadProfileData(dataService: DataService) { + dataService.viewerPublisher().sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] viewer in + self?.profileCardData = ProfileCardData( + name: viewer.name, + username: viewer.username, + imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } + ) + } + ) + .store(in: &subscriptions) + } } -public struct ProfileView: View { +struct ProfileView: View { + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService + + @ObservedObject private var viewModel = ProfileContainerViewModel() + @State private var showLogoutConfirmation = false - let profileCardData: ProfileCardData - let webAppBaseURL: URL - let actionHandler: (ProfileViewAction) -> Void - - public init( - profileCardData: ProfileCardData, - webAppBaseURL: URL, - actionHandler: @escaping (ProfileViewAction) -> Void - ) { - self.profileCardData = profileCardData - self.webAppBaseURL = webAppBaseURL - self.actionHandler = actionHandler - } - - public var body: some View { + var body: some View { #if os(iOS) Form { innerBody @@ -41,28 +50,32 @@ public struct ProfileView: View { private var innerBody: some View { Group { Section { - ProfileCard(data: profileCardData) - .onAppear { actionHandler(.loadProfileAction) } + ProfileCard(data: viewModel.profileCardData) + .onAppear { viewModel.loadProfileData(dataService: dataService) } + } + + Section { + NavigationLink(destination: EmailsView()) { + Text("Emails") + } } Section { NavigationLink( - destination: BasicWebAppView.privacyPolicyWebView(baseURL: webAppBaseURL) + destination: BasicWebAppView.privacyPolicyWebView(baseURL: dataService.appEnvironment.webAppBaseURL) ) { Text("Privacy Policy") } NavigationLink( - destination: BasicWebAppView.termsConditionsWebView(baseURL: webAppBaseURL) + destination: BasicWebAppView.termsConditionsWebView(baseURL: dataService.appEnvironment.webAppBaseURL) ) { Text("Terms and Conditions") } #if os(iOS) Button( - action: { - actionHandler(.showIntercomMessenger) - }, + action: { DataService.showIntercomMessenger?() }, label: { Text("Feedback") } ) #endif @@ -87,7 +100,7 @@ public struct ProfileView: View { Alert( title: Text("Are you sure you want to logout?"), primaryButton: .destructive(Text("Confirm")) { - actionHandler(.logout) + authenticator.logout() }, secondaryButton: .cancel() ) diff --git a/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift deleted file mode 100644 index d69c4735d..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/ProfileContainerView.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Combine -import Models -import Services -import SwiftUI -import Utils -import Views - -final class ProfileContainerViewModel: ObservableObject { - @Published var isLoading = false - @Published var profileCardData = ProfileCardData() - - var subscriptions = Set() - - func loadProfileData(dataService: DataService) { - dataService.viewerPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] viewer in - self?.profileCardData = ProfileCardData( - name: viewer.name, - username: viewer.username, - imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } - ) - } - ) - .store(in: &subscriptions) - } -} - -struct ProfileContainerView: View { - @EnvironmentObject var authenticator: Authenticator - @EnvironmentObject var dataService: DataService - - @ObservedObject private var viewModel = ProfileContainerViewModel() - - func actionHandler(action: ProfileViewAction) { - switch action { - case .loadProfileAction: - viewModel.loadProfileData(dataService: dataService) - case .logout: - authenticator.logout() - case .showIntercomMessenger: - DataService.showIntercomMessenger?() - } - } - - var body: some View { - ProfileView( - profileCardData: viewModel.profileCardData, - webAppBaseURL: dataService.appEnvironment.webAppBaseURL, - actionHandler: actionHandler - ) - } -} From e27c2bdd8be7d13b012e1413021d414b186f75c4 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 10:09:17 -0800 Subject: [PATCH 04/13] generate swift gql code --- .../Services/DataService/GQLSchema.swift | 390 +++++++++++++++++- packages/api/.env.example | 1 + 2 files changed, 385 insertions(+), 6 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index c94d2dddf..89528dbc1 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -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(selection: Selection) 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 = Selection } +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(selection: Selection) 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 = Selection +} + +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 = Selection +} + 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(input: InputObjects.SetLabelsInput, selection: Selection) 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(linkId: String, selection: Selection) throws -> Type { + func labels(selection: Selection) 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 = Selection } +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(setLabelsSuccess: Selection, setLabelsError: Selection) 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 = Selection +} + // 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 = .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 + } + } +} diff --git a/packages/api/.env.example b/packages/api/.env.example index fc982d2ea..2590b876d 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -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= \ No newline at end of file From 0f55e1ec1449f82d7fdc736ba83124a4197e6e63 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 11:00:18 -0800 Subject: [PATCH 05/13] create newsletter email publisher for apple apps --- .../Sources/Models/NewsletterEmails.swift | 21 +++++++ .../Queries/NewsletterEmailsQuery.swift | 63 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 apple/OmnivoreKit/Sources/Models/NewsletterEmails.swift create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift diff --git a/apple/OmnivoreKit/Sources/Models/NewsletterEmails.swift b/apple/OmnivoreKit/Sources/Models/NewsletterEmails.swift new file mode 100644 index 000000000..fde1f7017 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Models/NewsletterEmails.swift @@ -0,0 +1,21 @@ +import Foundation + +public class NewsletterEmails { + public let newsletterEmails: [NewsletterEmail] + + public init(newsletterEmails: [NewsletterEmail]) { + self.newsletterEmails = newsletterEmails + } +} + +public struct NewsletterEmail { + public let id: String + public let email: String + public let confirmationCode: String? + + public init(id: String, email: String, confirmationCode: String?) { + self.id = id + self.email = email + self.confirmationCode = confirmationCode + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift new file mode 100644 index 000000000..a9d7fcf28 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift @@ -0,0 +1,63 @@ +import Combine +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func newsletterEmailsPublisher() -> AnyPublisher { + enum QueryResult { + case success(result: NewsletterEmails) + case error(error: String) + } + + let newsletterEmailSelection = Selection.NewsletterEmail { + NewsletterEmail( + id: try $0.id(), + email: try $0.address(), + confirmationCode: try $0.confirmationCode() + ) + } + + let selection = Selection { + try $0.on( + newsletterEmailsSuccess: .init { + QueryResult.success(result: + NewsletterEmails( + newsletterEmails: 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() + } +} From a8ca27993f5cf48fa9161103e83ced189198fdb4 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 12:40:54 -0800 Subject: [PATCH 06/13] update newsletter swift query --- .../{NewsletterEmails.swift => NewsletterEmail.swift} | 8 -------- .../DataService/Queries/NewsletterEmailsQuery.swift | 10 +++------- 2 files changed, 3 insertions(+), 15 deletions(-) rename apple/OmnivoreKit/Sources/Models/{NewsletterEmails.swift => NewsletterEmail.swift} (61%) diff --git a/apple/OmnivoreKit/Sources/Models/NewsletterEmails.swift b/apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift similarity index 61% rename from apple/OmnivoreKit/Sources/Models/NewsletterEmails.swift rename to apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift index fde1f7017..8bd467055 100644 --- a/apple/OmnivoreKit/Sources/Models/NewsletterEmails.swift +++ b/apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift @@ -1,13 +1,5 @@ import Foundation -public class NewsletterEmails { - public let newsletterEmails: [NewsletterEmail] - - public init(newsletterEmails: [NewsletterEmail]) { - self.newsletterEmails = newsletterEmails - } -} - public struct NewsletterEmail { public let id: String public let email: String diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift index a9d7fcf28..af59ef863 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift @@ -4,9 +4,9 @@ import Models import SwiftGraphQL public extension DataService { - func newsletterEmailsPublisher() -> AnyPublisher { + func newsletterEmailsPublisher() -> AnyPublisher<[NewsletterEmail], ServerError> { enum QueryResult { - case success(result: NewsletterEmails) + case success(result: [NewsletterEmail]) case error(error: String) } @@ -21,11 +21,7 @@ public extension DataService { let selection = Selection { try $0.on( newsletterEmailsSuccess: .init { - QueryResult.success(result: - NewsletterEmails( - newsletterEmails: try $0.newsletterEmails(selection: newsletterEmailSelection.list) - ) - ) + QueryResult.success(result: try $0.newsletterEmails(selection: newsletterEmailSelection.list)) }, newsletterEmailsError: .init { QueryResult.error(error: try $0.errorCodes().description) From 666da89c987485a7ab9f746701f661a25a5ad219 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 13:10:59 -0800 Subject: [PATCH 07/13] fetch email list when loading email view --- .../App/Views/Profile/EmailsView.swift | 53 ------------ .../Views/Profile/NewsletterEmailsView.swift | 82 +++++++++++++++++++ .../App/Views/Profile/ProfileView.swift | 2 +- .../Sources/Models/NewsletterEmail.swift | 9 +- .../Queries/NewsletterEmailsQuery.swift | 2 +- 5 files changed, 89 insertions(+), 59 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift deleted file mode 100644 index 4036e1706..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/EmailsView.swift +++ /dev/null @@ -1,53 +0,0 @@ -import SwiftUI - -struct EmailsView: View { - let footerText = "Add PDFs to your library, or subscribe to emails using an Omnivore email address." - - @State var emails = [String]() - - var body: some View { - #if os(iOS) - Form { - innerBody - } - #elseif os(macOS) - List { - innerBody - } - .listStyle(InsetListStyle()) - #endif - } - - private var innerBody: some View { - Group { - Section(footer: Text(footerText)) { - Button( - action: { - withAnimation { - emails.insert("newemail@omnivore-relay.app\(emails.count)", at: 0) - } - }, - label: { - HStack { - Image(systemName: "plus.circle.fill").foregroundColor(.green) - Text("Create a new email address") - Spacer() - } - } - ) - } - - if !emails.isEmpty { - Section(header: Text("Existing Emails (Tap to copy)")) { - ForEach(emails, id: \.self) { email in - Button( - action: {}, - label: { Text(email) } - ) - } - } - } - } - .navigationTitle("Emails") - } -} diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift new file mode 100644 index 000000000..3f326d949 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -0,0 +1,82 @@ +import Combine +import Models +import Services +import SwiftUI + +final class NewsletterEmailsViewModel: ObservableObject { + private var hasLoadedInitialEmails = false + @Published var isLoading = false + @Published var emails = [NewsletterEmail]() + + var subscriptions = Set() + + 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) + } +} + +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: { + withAnimation { + print("create email") + } + }, + 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: {}, + label: { Text(newsletterEmail.email) } + ) + } + } + } + } + .navigationTitle("Emails") + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index b81554cb9..332e732b4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -55,7 +55,7 @@ struct ProfileView: View { } Section { - NavigationLink(destination: EmailsView()) { + NavigationLink(destination: NewsletterEmailsView()) { Text("Emails") } } diff --git a/apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift b/apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift index 8bd467055..ee4270668 100644 --- a/apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift +++ b/apple/OmnivoreKit/Sources/Models/NewsletterEmail.swift @@ -1,12 +1,13 @@ import Foundation -public struct NewsletterEmail { - public let id: String +public struct NewsletterEmail: Identifiable { + public let id = UUID() + public let emailId: String public let email: String public let confirmationCode: String? - public init(id: String, email: String, confirmationCode: String?) { - self.id = id + public init(emailId: String, email: String, confirmationCode: String?) { + self.emailId = emailId self.email = email self.confirmationCode = confirmationCode } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift index af59ef863..25ccea69f 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift @@ -12,7 +12,7 @@ public extension DataService { let newsletterEmailSelection = Selection.NewsletterEmail { NewsletterEmail( - id: try $0.id(), + emailId: try $0.id(), email: try $0.address(), confirmationCode: try $0.confirmationCode() ) From 5f0297f237a03abed3d143a429514d42b08fb1ab Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 13:13:51 -0800 Subject: [PATCH 08/13] copy text to pasteboard when tapping email button --- .../Sources/App/Views/Profile/NewsletterEmailsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift index 3f326d949..58d00977e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -70,7 +70,7 @@ struct NewsletterEmailsView: View { Section(header: Text("Existing Emails (Tap to copy)")) { ForEach(viewModel.emails) { newsletterEmail in Button( - action: {}, + action: { UIPasteboard.general.string = newsletterEmail.email }, label: { Text(newsletterEmail.email) } ) } From f6ac43373e52f4943a09ffa9c89791248e63f30f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 13:26:41 -0800 Subject: [PATCH 09/13] add createNewsletterEmailPublisher --- .../CreateNewsletterEmailMutation.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift new file mode 100644 index 000000000..b8028e47e --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift @@ -0,0 +1,60 @@ +import Combine +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func createNewsletterEmailPublisher() -> AnyPublisher { + enum MutationResult { + case saved(newsletterEmail: NewsletterEmail) + case error(errorCode: Enums.CreateNewsletterEmailErrorCode) + } + + let selection = Selection { + 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() + } +} From a5f6eae574eb3b9b8b9a86aed703883a9e0e16c2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 13:38:00 -0800 Subject: [PATCH 10/13] create an email when user taps crete email button --- .../Views/Profile/NewsletterEmailsView.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift index 58d00977e..3c3752e70 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -23,6 +23,21 @@ final class NewsletterEmailsViewModel: ObservableObject { ) .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 { @@ -51,9 +66,7 @@ struct NewsletterEmailsView: View { Section(footer: Text(footerText)) { Button( action: { - withAnimation { - print("create email") - } + viewModel.createEmail(dataService: dataService) }, label: { HStack { From 0a9b9f138723747742f495ff03afceee8b77f59b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 25 Feb 2022 15:00:43 -0800 Subject: [PATCH 11/13] Add a snackbar message when an email address is copied --- .../Sources/App/Views/Profile/NewsletterEmailsView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift index 3c3752e70..4e930e4fa 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -83,7 +83,10 @@ struct NewsletterEmailsView: View { Section(header: Text("Existing Emails (Tap to copy)")) { ForEach(viewModel.emails) { newsletterEmail in Button( - action: { UIPasteboard.general.string = newsletterEmail.email }, + action: { + UIPasteboard.general.string = newsletterEmail.email + NSNotification.operationSuccess(message: "Email copied") + }, label: { Text(newsletterEmail.email) } ) } From 629c378244263e36214c831d4825481c4b4cdb96 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 19:23:29 -0800 Subject: [PATCH 12/13] use NSPasteboard for macos copying --- .../App/Views/Profile/NewsletterEmailsView.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift index 4e930e4fa..1eaf65a44 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -84,7 +84,16 @@ struct NewsletterEmailsView: View { ForEach(viewModel.emails) { newsletterEmail in Button( action: { - UIPasteboard.general.string = newsletterEmail.email + #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 + NSNotification.operationSuccess(message: "Email copied") }, label: { Text(newsletterEmail.email) } From ce5123e2d7171113aa432ab2cb9f57bc3814aa7b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 25 Feb 2022 19:39:12 -0800 Subject: [PATCH 13/13] create helper function on Snackbar to show it --- apple/OmnivoreKit/Sources/App/SnackbarExtension.swift | 9 +++++++++ .../OmnivoreKit/Sources/App/Views/HomeFeedView.swift | 11 +++++------ .../App/Views/Profile/NewsletterEmailsView.swift | 3 ++- apple/OmnivoreKit/Sources/Views/SnackBar.swift | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/SnackbarExtension.swift diff --git a/apple/OmnivoreKit/Sources/App/SnackbarExtension.swift b/apple/OmnivoreKit/Sources/App/SnackbarExtension.swift new file mode 100644 index 000000000..fab837616 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/SnackbarExtension.swift @@ -0,0 +1,9 @@ +import Foundation +import Services +import Views + +extension Snackbar { + static func show(message: String) { + NSNotification.operationSuccess(message: message) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift b/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift index 7f5d2fc46..13cb2ca1d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/HomeFeedView.swift @@ -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) } } ) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift index 1eaf65a44..0e7063d36 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -2,6 +2,7 @@ import Combine import Models import Services import SwiftUI +import Views final class NewsletterEmailsViewModel: ObservableObject { private var hasLoadedInitialEmails = false @@ -94,7 +95,7 @@ struct NewsletterEmailsView: View { pasteBoard.writeObjects([newsletterEmail.email as NSString]) #endif - NSNotification.operationSuccess(message: "Email copied") + Snackbar.show(message: "Email copied") }, label: { Text(newsletterEmail.email) } ) diff --git a/apple/OmnivoreKit/Sources/Views/SnackBar.swift b/apple/OmnivoreKit/Sources/Views/SnackBar.swift index aed304e18..89b8b86f5 100644 --- a/apple/OmnivoreKit/Sources/Views/SnackBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SnackBar.swift @@ -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