From caefa364a89243e8eb3240c2b957182e1c603027 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 31 Mar 2022 22:02:35 -0700 Subject: [PATCH 01/14] add lables to feedItem type --- .../OmnivoreKit/Sources/Models/FeedItem.swift | 9 ++- .../Sources/Models/FeedItemLabel.swift | 23 ++++++ .../Services/DataService/GQLSchema.swift | 78 +++++++++++++++++-- .../Queries/LibraryItemsQuery.swift | 13 +++- 4 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Models/FeedItemLabel.swift diff --git a/apple/OmnivoreKit/Sources/Models/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/FeedItem.swift index 30960fbb7..af6a53139 100644 --- a/apple/OmnivoreKit/Sources/Models/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/FeedItem.swift @@ -28,6 +28,7 @@ public struct FeedItem: Identifiable, Hashable, Decodable { public let slug: String public let isArchived: Bool public let contentReader: String? + public let labels: [FeedItemLabel] public init( id: String, @@ -46,7 +47,8 @@ public struct FeedItem: Identifiable, Hashable, Decodable { publishDate: Date?, slug: String, isArchived: Bool, - contentReader: String? + contentReader: String?, + labels: [FeedItemLabel] ) { self.id = id self.title = title @@ -65,10 +67,12 @@ public struct FeedItem: Identifiable, Hashable, Decodable { self.slug = slug self.isArchived = isArchived self.contentReader = contentReader + self.labels = labels } + // swiftlint:disable:next line_length enum CodingKeys: String, CodingKey { - case id, title, createdAt, savedAt, image, isArchived, readingProgressPercent, readingProgressAnchorIndex, slug, contentReader, url + case id, title, createdAt, savedAt, image, isArchived, readingProgressPercent, readingProgressAnchorIndex, slug, contentReader, url, labels } public init(from decoder: Decoder) throws { @@ -85,6 +89,7 @@ public struct FeedItem: Identifiable, Hashable, Decodable { contentReader = try container.decode(String.self, forKey: .contentReader) pageURLString = try container.decode(String.self, forKey: .url) isArchived = try container.decode(Bool.self, forKey: .isArchived) + labels = try container.decode([FeedItemLabel].self, forKey: .labels) self.onDeviceImageURLString = nil self.documentDirectoryPath = nil diff --git a/apple/OmnivoreKit/Sources/Models/FeedItemLabel.swift b/apple/OmnivoreKit/Sources/Models/FeedItemLabel.swift new file mode 100644 index 000000000..14a157a02 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Models/FeedItemLabel.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct FeedItemLabel: Decodable, Hashable { + public let id: String + public let name: String + public let color: String + public let createdAt: Date? + public let description: String? + + public init( + id: String, + name: String, + color: String, + createdAt: Date?, + description: String? + ) { + self.id = id + self.name = name + self.color = color + self.createdAt = createdAt + self.description = description + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index 89528dbc1..6f50d6b8b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -2970,8 +2970,11 @@ extension Objects { let savedByViewer: [String: Bool] let shareInfo: [String: Objects.LinkShareInfo] let sharedComment: [String: String] + let siteIcon: [String: String] + let siteName: [String: String] let slug: [String: String] let title: [String: String] + let uploadFileId: [String: String] let url: [String: String] enum TypeName: String, Codable { @@ -3088,6 +3091,14 @@ extension Objects.Article: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "siteIcon": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "siteName": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "slug": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -3096,6 +3107,10 @@ extension Objects.Article: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "uploadFileId": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "url": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -3134,8 +3149,11 @@ extension Objects.Article: Decodable { savedByViewer = map["savedByViewer"] shareInfo = map["shareInfo"] sharedComment = map["sharedComment"] + siteIcon = map["siteIcon"] + siteName = map["siteName"] slug = map["slug"] title = map["title"] + uploadFileId = map["uploadFileId"] url = map["url"] } } @@ -3587,6 +3605,51 @@ extension Fields where TypeLock == Objects.Article { return selection.mock() } } + + func uploadFileId() throws -> String? { + let field = GraphQLField.leaf( + name: "uploadFileId", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.uploadFileId[field.alias!] + case .mocking: + return nil + } + } + + func siteName() throws -> String? { + let field = GraphQLField.leaf( + name: "siteName", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.siteName[field.alias!] + case .mocking: + return nil + } + } + + func siteIcon() throws -> String? { + let field = GraphQLField.leaf( + name: "siteIcon", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.siteIcon[field.alias!] + case .mocking: + return nil + } + } } extension Selection where TypeLock == Never, Type == Never { @@ -10784,7 +10847,7 @@ extension Fields where TypeLock == Objects.Label { } } - func createdAt() throws -> DateTime { + func createdAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "createdAt", arguments: [] @@ -10793,12 +10856,9 @@ extension Fields where TypeLock == Objects.Label { switch response { case let .decoding(data): - if let data = data.createdAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.createdAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } } @@ -16779,6 +16839,10 @@ extension Enums { /// SortBy enum SortBy: String, CaseIterable, Codable { case updatedTime = "UPDATED_TIME" + + case score = "SCORE" + + case savedAt = "SAVED_AT" } } @@ -16956,6 +17020,8 @@ extension Enums { case payloadTooLarge = "PAYLOAD_TOO_LARGE" case uploadFileMissing = "UPLOAD_FILE_MISSING" + + case elasticError = "ELASTIC_ERROR" } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift index 7419e2649..174df3d26 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift @@ -143,7 +143,18 @@ let homeFeedItemSelection = Selection.Article { publishDate: try $0.publishedAt()?.value, slug: try $0.slug(), isArchived: try $0.isArchived(), - contentReader: try $0.contentReader().rawValue + contentReader: try $0.contentReader().rawValue, + labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] + ) +} + +private let feedItemLabelSelection = Selection.Label { + FeedItemLabel( + id: try $0.id(), + name: try $0.name(), + color: try $0.color(), + createdAt: try $0.createdAt()?.value, + description: try $0.description() ) } From f0034f8167aa8f5c2a40095d80ad42e75b7831fb Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 31 Mar 2022 22:15:22 -0700 Subject: [PATCH 02/14] swiftlint fixes --- .../Sources/App/Views/WebReader/WebReaderViewModel.swift | 7 ++----- apple/OmnivoreKit/Sources/Models/FeedItem.swift | 2 +- .../Services/DataService/Queries/ArticleContentQuery.swift | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 5764bac83..2afceda35 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -10,11 +10,8 @@ struct SafariWebLink: Identifiable { } func encodeHighlightResult(_ highlight: Highlight) -> [String: Any]? { - let data = try? JSONEncoder().encode(highlight) - if let data = data, let dictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] { - return dictionary - } - return nil + guard let data = try? JSONEncoder().encode(highlight) else { return nil } + return try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] } final class WebReaderViewModel: ObservableObject { diff --git a/apple/OmnivoreKit/Sources/Models/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/FeedItem.swift index af6a53139..6d716919b 100644 --- a/apple/OmnivoreKit/Sources/Models/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/FeedItem.swift @@ -70,8 +70,8 @@ public struct FeedItem: Identifiable, Hashable, Decodable { self.labels = labels } - // swiftlint:disable:next line_length enum CodingKeys: String, CodingKey { + // swiftlint:disable:next line_length case id, title, createdAt, savedAt, image, isArchived, readingProgressPercent, readingProgressAnchorIndex, slug, contentReader, url, labels } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index 4970c2330..8822a0d98 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -3,7 +3,6 @@ import Foundation import Models import SwiftGraphQL -// swiftlint:disable:next function_body_length public extension DataService { func articleContentPublisher(username: String, slug: String) -> AnyPublisher { enum QueryResult { From 9519be128c232a73a97daf6a91066f35be55d50d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 31 Mar 2022 22:29:50 -0700 Subject: [PATCH 03/14] add a createLabelPublisher --- .../Mutations/CreateLabelPublisher.swift | 62 +++++++++++++++++++ .../Queries/LibraryItemsQuery.swift | 10 --- .../Selections/FeedItemLabelSelection.swift | 12 ++++ 3 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Selections/FeedItemLabelSelection.swift diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift new file mode 100644 index 000000000..6400f87cf --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift @@ -0,0 +1,62 @@ +import Combine +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func createLabelPublisher( + name: String, + color: String, + description: String? + ) -> AnyPublisher { + enum MutationResult { + case saved(label: FeedItemLabel) + case error(errorCode: Enums.CreateLabelErrorCode) + } + + let selection = Selection { + try $0.on( + createLabelSuccess: .init { .saved(label: try $0.label(selection: feedItemLabelSelection)) }, + createLabelError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) } + ) + } + + let mutation = Selection.Mutation { + try $0.createLabel( + input: InputObjects.CreateLabelInput( + name: name, + color: color, + description: OptionalArgument(description) + ), + 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(label: label): + promise(.success(label)) + case let .error(errorCode: errorCode): + promise(.failure(.message(messageText: errorCode.rawValue))) + } + case .failure: + promise(.failure(.message(messageText: "graphql error"))) + } + } + } + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift index 174df3d26..532ffe88f 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift @@ -148,16 +148,6 @@ let homeFeedItemSelection = Selection.Article { ) } -private let feedItemLabelSelection = Selection.Label { - FeedItemLabel( - id: try $0.id(), - name: try $0.name(), - color: try $0.color(), - createdAt: try $0.createdAt()?.value, - description: try $0.description() - ) -} - private let articleEdgeSelection = Selection.ArticleEdge { try $0.node(selection: homeFeedItemSelection) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/FeedItemLabelSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/FeedItemLabelSelection.swift new file mode 100644 index 000000000..998dc8dea --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/FeedItemLabelSelection.swift @@ -0,0 +1,12 @@ +import Models +import SwiftGraphQL + +let feedItemLabelSelection = Selection.Label { + FeedItemLabel( + id: try $0.id(), + name: try $0.name(), + color: try $0.color(), + createdAt: try $0.createdAt()?.value, + description: try $0.description() + ) +} From db11a75107aacf4ba7a1d5321560db69bf2624d5 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 31 Mar 2022 22:53:03 -0700 Subject: [PATCH 04/14] stub in labels view --- .../App/Views/Profile/LabelsView.swift | 93 +++++++++++++++++++ .../App/Views/Profile/ProfileView.swift | 4 + 2 files changed, 97 insertions(+) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift new file mode 100644 index 000000000..830389bb3 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -0,0 +1,93 @@ +import Combine +import Models +import Services +import SwiftUI +import Views + +final class LabelsViewModel: ObservableObject { + private var hasLoadedInitialLabels = false + @Published var isLoading = false + @Published var labels = [FeedItemLabel]() + + var subscriptions = Set() + + func loadLabels(dataService _: DataService) { + isLoading = true + +// dataService.newsletterEmailsPublisher().sink( +// receiveCompletion: { _ in }, +// receiveValue: { [weak self] result in +// self?.isLoading = false +// self?.emails = result +// self?.hasLoadedInitialLabels = true +// } +// ) +// .store(in: &subscriptions) + } + + func createLabel(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 LabelsView: View { + @EnvironmentObject var dataService: DataService + @StateObject var viewModel = LabelsViewModel() + let footerText = "Use labels to create curated collections of links." + + var body: some View { + Group { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } + .onAppear { viewModel.loadLabels(dataService: dataService) } + } + + private var innerBody: some View { + Group { + Section(footer: Text(footerText)) { + Button( + action: { + viewModel.createLabel(dataService: dataService) + }, + label: { + HStack { + Image(systemName: "plus.circle.fill").foregroundColor(.green) + Text("Create a new label") + Spacer() + } + } + ) + .disabled(viewModel.isLoading) + } + + if !viewModel.labels.isEmpty { + Section(header: Text("Labels")) { + ForEach(viewModel.labels, id: \.id) { label in + Text(label.name) + } + } + } + } + .navigationTitle("Labels") + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index ca2443130..7cf3001e7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -63,6 +63,10 @@ struct ProfileView: View { } Section { + NavigationLink(destination: LabelsView()) { + Text("Labels") + } + NavigationLink(destination: NewsletterEmailsView()) { Text("Emails") } From dcc79245313c9914ad1a88e5b7db174561ab4a2f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 31 Mar 2022 23:03:51 -0700 Subject: [PATCH 05/14] create labelsPublisher to fetch existing labels --- .../App/Views/Profile/LabelsView.swift | 20 ++++---- .../DataService/Queries/LabelsPublisher.swift | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift index 830389bb3..054fb2b11 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -11,18 +11,18 @@ final class LabelsViewModel: ObservableObject { var subscriptions = Set() - func loadLabels(dataService _: DataService) { + func loadLabels(dataService: DataService) { isLoading = true -// dataService.newsletterEmailsPublisher().sink( -// receiveCompletion: { _ in }, -// receiveValue: { [weak self] result in -// self?.isLoading = false -// self?.emails = result -// self?.hasLoadedInitialLabels = true -// } -// ) -// .store(in: &subscriptions) + dataService.labelsPublisher().sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] result in + self?.isLoading = false + self?.labels = result + self?.hasLoadedInitialLabels = true + } + ) + .store(in: &subscriptions) } func createLabel(dataService _: DataService) { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift new file mode 100644 index 000000000..5cce49647 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift @@ -0,0 +1,49 @@ +import Combine +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func labelsPublisher() -> AnyPublisher<[FeedItemLabel], ServerError> { + enum QueryResult { + case success(result: [FeedItemLabel]) + case error(error: String) + } + + let selection = Selection { + try $0.on(labelsSuccess: .init { + QueryResult.success(result: try $0.labels(selection: feedItemLabelSelection.list)) + }, + labelsError: .init { + QueryResult.error(error: try $0.errorCodes().description) + }) + } + + let query = Selection.Query { + try $0.labels(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 72438d2d97a9e3c0e4d229c03e784feb94704218 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 31 Mar 2022 23:13:06 -0700 Subject: [PATCH 06/14] stub in test label mutation --- .../App/Views/Profile/LabelsView.swift | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift index 054fb2b11..81688c362 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -25,19 +25,23 @@ final class LabelsViewModel: ObservableObject { .store(in: &subscriptions) } - func createLabel(dataService _: DataService) { + func createLabel(dataService: DataService, name: String, color: String, description: String?) { 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) + dataService.createLabelPublisher( + name: name, + color: color, + description: description + ).sink( + receiveCompletion: { [weak self] _ in + self?.isLoading = false + }, + receiveValue: { [weak self] result in + self?.isLoading = false + self?.labels.insert(result, at: 0) + } + ) + .store(in: &subscriptions) } } @@ -67,7 +71,12 @@ struct LabelsView: View { Section(footer: Text(footerText)) { Button( action: { - viewModel.createLabel(dataService: dataService) + viewModel.createLabel( + dataService: dataService, + name: "ios-test", + color: "#F9D354", + description: "hardcoded test label" + ) }, label: { HStack { From 90da2e29882af58d7ae4f9e4b66b170715cbc121 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 1 Apr 2022 09:14:02 -0700 Subject: [PATCH 07/14] add helper functions for Color/hex conversions --- .../App/Views/Profile/LabelsView.swift | 29 ++++--- .../App/Views/Profile/ProfileView.swift | 6 +- .../Sources/Utils/ColorUtils.swift | 79 +++++++++++++++++++ .../Sources/Utils/FeatureFlags.swift | 2 +- .../Sources/Views/FeedItem/GridCard.swift | 2 +- 5 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Utils/ColorUtils.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift index 81688c362..24181b8d6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -25,12 +25,12 @@ final class LabelsViewModel: ObservableObject { .store(in: &subscriptions) } - func createLabel(dataService: DataService, name: String, color: String, description: String?) { + func createLabel(dataService: DataService, name: String, color: Color, description: String?) { isLoading = true dataService.createLabelPublisher( name: name, - color: color, + color: color.hex ?? "", description: description ).sink( receiveCompletion: { [weak self] _ in @@ -48,6 +48,10 @@ final class LabelsViewModel: ObservableObject { struct LabelsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = LabelsViewModel() + + @State private var newLabelName = "" + @State private var newLabelColor = Color.clear + let footerText = "Use labels to create curated collections of links." var body: some View { @@ -69,24 +73,23 @@ struct LabelsView: View { private var innerBody: some View { Group { Section(footer: Text(footerText)) { + TextField("Label Name", text: $newLabelName) + ColorPicker( + newLabelColor == .clear ? "Select Color" : newLabelColor.description, + selection: $newLabelColor + ) Button( action: { viewModel.createLabel( dataService: dataService, - name: "ios-test", - color: "#F9D354", - description: "hardcoded test label" + name: newLabelName, + color: newLabelColor, + description: nil ) }, - label: { - HStack { - Image(systemName: "plus.circle.fill").foregroundColor(.green) - Text("Create a new label") - Spacer() - } - } + label: { Text("Create Label") } ) - .disabled(viewModel.isLoading) + .disabled(viewModel.isLoading || newLabelName.isEmpty || newLabelColor == .clear) } if !viewModel.labels.isEmpty { diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 7cf3001e7..80d6eef94 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -63,8 +63,10 @@ struct ProfileView: View { } Section { - NavigationLink(destination: LabelsView()) { - Text("Labels") + if FeatureFlag.enableLabels { + NavigationLink(destination: LabelsView()) { + Text("Labels") + } } NavigationLink(destination: NewsletterEmailsView()) { diff --git a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift new file mode 100644 index 000000000..faad3bbbc --- /dev/null +++ b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift @@ -0,0 +1,79 @@ +import SwiftUI + +public extension Color { + /// Inititializes a `Color` from a hex value + /// - Parameter hex: Color hex value. ex: `#FFFFFF` + /// + init?(hex: String) { + var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") + + var rgb: UInt64 = 0 + + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 1.0 + + let length = hexSanitized.count + + guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil } + + if length == 6 { + red = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 + green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 + blue = CGFloat(rgb & 0x0000FF) / 255.0 + + } else if length == 8 { + red = CGFloat((rgb & 0xFF00_0000) >> 24) / 255.0 + green = CGFloat((rgb & 0x00FF_0000) >> 16) / 255.0 + blue = CGFloat((rgb & 0x0000_FF00) >> 8) / 255.0 + alpha = CGFloat(rgb & 0x0000_00FF) / 255.0 + + } else { + return nil + } + + self.init(red: red, green: green, blue: blue, opacity: alpha) + } + + var hex: String? { + if let hex = toHex() { + return "#\(hex)" + } else { + return nil + } + } + + private func toHex() -> String? { + let uic = UIColor(self) + guard let components = uic.cgColor.components, components.count >= 3 else { + return nil + } + let red = Float(components[0]) + let green = Float(components[1]) + let blue = Float(components[2]) + var alpha = Float(1.0) + + if components.count >= 4 { + alpha = Float(components[3]) + } + + if alpha != Float(1.0) { + return String( + format: "%02lX%02lX%02lX%02lX", + lroundf(red * 255), + lroundf(green * 255), + lroundf(blue * 255), + lroundf(alpha * 255) + ) + } else { + return String( + format: "%02lX%02lX%02lX", + lroundf(red * 255), + lroundf(green * 255), + lroundf(blue * 255) + ) + } + } +} diff --git a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift index a06f1d9e8..878fb6145 100644 --- a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift +++ b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift @@ -14,6 +14,6 @@ public enum FeatureFlag { public static let enablePushNotifications = false public static let enableShareButton = false public static let enableSnooze = false - public static let showFeedItemTags = false + public static let enableLabels = false public static let useLocalWebView = true } diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index e098badda..2ee1f1821 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -156,7 +156,7 @@ public struct GridCard: View { .onTapGesture { tapHandler() } // Category Labels - if FeatureFlag.showFeedItemTags { + if FeatureFlag.enableLabels { ScrollView(.horizontal, showsIndicators: false) { HStack { TextChip(text: "label", color: .red) From 68fe58d9f35c4964703e470f10f9a41f3f900dcf Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 1 Apr 2022 09:21:03 -0700 Subject: [PATCH 08/14] rename local hex var to hexValue to make it distinct from property name --- apple/OmnivoreKit/Sources/Utils/ColorUtils.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift index faad3bbbc..66d5f4bc2 100644 --- a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift +++ b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift @@ -38,8 +38,8 @@ public extension Color { } var hex: String? { - if let hex = toHex() { - return "#\(hex)" + if let hexValue = toHex() { + return "#\(hexValue)" } else { return nil } From da78a2a9ce22540bc6c369a989acfd2d3ba0a539 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 4 Apr 2022 09:06:19 -0700 Subject: [PATCH 09/14] add a create label modal --- .../App/Views/Profile/LabelsView.swift | 68 ++++++++++++++++--- .../Sources/Utils/FeatureFlags.swift | 2 +- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift index 24181b8d6..b2f95acda 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -8,6 +8,7 @@ final class LabelsViewModel: ObservableObject { private var hasLoadedInitialLabels = false @Published var isLoading = false @Published var labels = [FeedItemLabel]() + @Published var showCreateEmailModal = false var subscriptions = Set() @@ -39,6 +40,7 @@ final class LabelsViewModel: ObservableObject { receiveValue: { [weak self] result in self?.isLoading = false self?.labels.insert(result, at: 0) + self?.showCreateEmailModal = false } ) .store(in: &subscriptions) @@ -49,9 +51,6 @@ struct LabelsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = LabelsViewModel() - @State private var newLabelName = "" - @State private var newLabelColor = Color.clear - let footerText = "Use labels to create curated collections of links." var body: some View { @@ -73,7 +72,47 @@ struct LabelsView: View { private var innerBody: some View { Group { Section(footer: Text(footerText)) { + Button( + action: { viewModel.showCreateEmailModal = true }, + label: { + HStack { + Image(systemName: "plus.circle.fill").foregroundColor(.green) + Text("Create a new Label") + Spacer() + } + } + ) + .disabled(viewModel.isLoading) + } + + if !viewModel.labels.isEmpty { + Section(header: Text("Labels")) { + ForEach(viewModel.labels, id: \.id) { label in + Text(label.name) + } + } + } + } + .navigationTitle("Labels") + .sheet(isPresented: $viewModel.showCreateEmailModal) { + CreateLabelView(viewModel: viewModel) + } + } +} + +struct CreateLabelView: View { + @EnvironmentObject var dataService: DataService + @ObservedObject var viewModel: LabelsViewModel + + @State private var newLabelName = "" + @State private var newLabelColor = Color.clear + + var body: some View { + NavigationView { + VStack(spacing: 16) { TextField("Label Name", text: $newLabelName) + .keyboardType(.alphabet) + .textFieldStyle(StandardTextFieldStyle()) ColorPicker( newLabelColor == .clear ? "Select Color" : newLabelColor.description, selection: $newLabelColor @@ -87,19 +126,26 @@ struct LabelsView: View { description: nil ) }, - label: { Text("Create Label") } + label: { Text("Create") } ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) .disabled(viewModel.isLoading || newLabelName.isEmpty || newLabelColor == .clear) + Spacer() } - - if !viewModel.labels.isEmpty { - Section(header: Text("Labels")) { - ForEach(viewModel.labels, id: \.id) { label in - Text(label.name) - } + .padding() + .toolbar { + ToolbarItem(placement: .automatic) { + Button( + action: { viewModel.showCreateEmailModal = false }, + label: { + Image(systemName: "xmark") + .foregroundColor(.appGrayTextContrast) + } + ) } } + .navigationTitle("Create New Label") + .navigationBarTitleDisplayMode(.inline) } - .navigationTitle("Labels") } } diff --git a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift index 878fb6145..ab5196f5a 100644 --- a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift +++ b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift @@ -14,6 +14,6 @@ public enum FeatureFlag { public static let enablePushNotifications = false public static let enableShareButton = false public static let enableSnooze = false - public static let enableLabels = false + public static let enableLabels = true public static let useLocalWebView = true } From 20a9cb811f69823f63091e375b1a0c1d9e18f55f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 5 Apr 2022 08:47:14 -0700 Subject: [PATCH 10/14] stub in edit labels modal --- .../App/Views/Home/Components/FeedCardNavigationLink.swift | 2 +- .../OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift | 5 +++++ .../Sources/App/Views/Home/HomeFeedViewModel.swift | 1 + apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift | 5 +++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 14916f3db..9db48b14a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -68,7 +68,7 @@ struct GridCardNavigationLink: View { viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService) } } - .aspectRatio(2.1, contentMode: .fill) + .aspectRatio(1.8, contentMode: .fill) .scaleEffect(scale) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index a6c17d60f..c621e4f12 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -167,6 +167,9 @@ import Views } } } + .sheet(item: $viewModel.itemUnderLabelEdit, onDismiss: { print("edit label modal dismissed") }) { item in + Text("editing item with id: \(item.id)") + } } } } @@ -323,6 +326,8 @@ import Views case .delete: itemToRemove = item confirmationShown = true + case .editLabels: + viewModel.itemUnderLabelEdit = item } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 84408f83e..bf71dfd18 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -14,6 +14,7 @@ final class HomeFeedViewModel: ObservableObject { @Published var items = [FeedItem]() @Published var isLoading = false @Published var showPushNotificationPrimer = false + @Published var itemUnderLabelEdit: FeedItem? var cursor: String? var sendProgressUpdates = false diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index 2ee1f1821..ced09d0c4 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -5,6 +5,7 @@ import Utils public enum GridCardAction { case toggleArchiveStatus case delete + case editLabels } public struct GridCard: View { @@ -42,6 +43,10 @@ public struct GridCard: View { var contextMenuView: some View { Group { + Button( + action: { menuActionHandler(.editLabels) }, + label: { Label("Edit Labels", systemImage: "tag") } + ) Button( action: { menuActionHandler(.toggleArchiveStatus) }, label: { From f34631525b0f7fd986bca2e2ad101ca94bb7bd19 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 5 Apr 2022 11:49:45 -0700 Subject: [PATCH 11/14] create apply labels modal --- .../Sources/App/Views/ApplyLabelsView.swift | 70 +++++++++++++++++++ .../App/Views/Home/HomeFeedViewIOS.swift | 2 +- .../App/Views/Profile/LabelsView.swift | 1 + 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift new file mode 100644 index 000000000..a24826f8f --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift @@ -0,0 +1,70 @@ +import Combine +import Models +import Services +import SwiftUI + +final class ApplyLabelsViewModel: ObservableObject { + private var hasLoadedInitialLabels = false + @Published var isLoading = true + @Published var selectedLabels = Set() + @Published var labels = [FeedItemLabel]() + + var subscriptions = Set() + + func load(item: FeedItem, dataService: DataService) { + guard !hasLoadedInitialLabels else { return } + + dataService.labelsPublisher().sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] result in + self?.isLoading = false + self?.labels = result + self?.hasLoadedInitialLabels = true + self?.selectedLabels = Set(item.labels) + } + ) + .store(in: &subscriptions) + } +} + +struct ApplyLabelsView: View { + let item: FeedItem + @EnvironmentObject var dataService: DataService + @Environment(\.presentationMode) private var presentationMode + @StateObject var viewModel = ApplyLabelsViewModel() + + var body: some View { + NavigationView { + if viewModel.isLoading { + EmptyView() + } else { + List(viewModel.labels, id: \.self, selection: $viewModel.selectedLabels) { label in + Text(label.name) + } + .environment(\.editMode, .constant(EditMode.active)) + .navigationTitle("Apply Labels") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Text("Cancel") } + ) + } + ToolbarItem(placement: .navigationBarTrailing) { + Button( + action: { + print("saving") + presentationMode.wrappedValue.dismiss() + }, + label: { Text("Save") } + ) + } + } + } + } + .onAppear { + viewModel.load(item: item, dataService: dataService) + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index c621e4f12..1f908cbe6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -168,7 +168,7 @@ import Views } } .sheet(item: $viewModel.itemUnderLabelEdit, onDismiss: { print("edit label modal dismissed") }) { item in - Text("editing item with id: \(item.id)") + ApplyLabelsView(item: item) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift index b2f95acda..9853cb993 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -13,6 +13,7 @@ final class LabelsViewModel: ObservableObject { var subscriptions = Set() func loadLabels(dataService: DataService) { + guard !hasLoadedInitialLabels else { return } isLoading = true dataService.labelsPublisher().sink( From 3742ee0330f9df91e73a9396de80518b3caabdc0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 5 Apr 2022 13:45:39 -0700 Subject: [PATCH 12/14] sync article labels with serevr --- .../Sources/App/Views/ApplyLabelsView.swift | 23 +++++++- .../App/Views/Home/HomeFeedViewIOS.swift | 6 +- .../App/Views/Home/HomeFeedViewModel.swift | 7 +++ .../OmnivoreKit/Sources/Models/FeedItem.swift | 2 +- .../UpdateArticleLabelsPublisher.swift | 57 +++++++++++++++++++ .../Sources/Views/FeedItem/GridCard.swift | 5 +- .../OmnivoreKit/Sources/Views/TextChip.swift | 18 +++++- 7 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift index a24826f8f..ffb6214fb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift @@ -2,6 +2,7 @@ import Combine import Models import Services import SwiftUI +import Views final class ApplyLabelsViewModel: ObservableObject { private var hasLoadedInitialLabels = false @@ -25,10 +26,20 @@ final class ApplyLabelsViewModel: ObservableObject { ) .store(in: &subscriptions) } + + func saveChanges(itemID: String, dataService: DataService, onComplete: @escaping ([FeedItemLabel]) -> Void) { + dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabels.map(\.id)).sink( + receiveCompletion: { _ in }, + receiveValue: { onComplete($0) } + ) + .store(in: &subscriptions) + } } struct ApplyLabelsView: View { let item: FeedItem + let commitLabelChanges: ([FeedItemLabel]) -> Void + @EnvironmentObject var dataService: DataService @Environment(\.presentationMode) private var presentationMode @StateObject var viewModel = ApplyLabelsViewModel() @@ -39,7 +50,11 @@ struct ApplyLabelsView: View { EmptyView() } else { List(viewModel.labels, id: \.self, selection: $viewModel.selectedLabels) { label in - Text(label.name) + if let textChip = TextChip(feedItemLabel: label) { + textChip + } else { + Text(label.name) + } } .environment(\.editMode, .constant(EditMode.active)) .navigationTitle("Apply Labels") @@ -54,8 +69,10 @@ struct ApplyLabelsView: View { ToolbarItem(placement: .navigationBarTrailing) { Button( action: { - print("saving") - presentationMode.wrappedValue.dismiss() + viewModel.saveChanges(itemID: item.id, dataService: dataService) { labels in + commitLabelChanges(labels) + presentationMode.wrappedValue.dismiss() + } }, label: { Text("Save") } ) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 1f908cbe6..535eb577e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -167,8 +167,10 @@ import Views } } } - .sheet(item: $viewModel.itemUnderLabelEdit, onDismiss: { print("edit label modal dismissed") }) { item in - ApplyLabelsView(item: item) + .sheet(item: $viewModel.itemUnderLabelEdit) { item in + ApplyLabelsView(item: item) { labels in + viewModel.updateLabels(itemID: item.id, labels: labels) + } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index bf71dfd18..f5e304094 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -183,4 +183,11 @@ final class HomeFeedViewModel: ObservableObject { items[index].readingProgress = progress } } + + func updateLabels(itemID: String, labels: [FeedItemLabel]) { + guard let item = items.first(where: { $0.id == itemID }) else { return } + if let index = items.firstIndex(of: item) { + items[index].labels = labels + } + } } diff --git a/apple/OmnivoreKit/Sources/Models/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/FeedItem.swift index 6d716919b..42ff019ea 100644 --- a/apple/OmnivoreKit/Sources/Models/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/FeedItem.swift @@ -28,7 +28,7 @@ public struct FeedItem: Identifiable, Hashable, Decodable { public let slug: String public let isArchived: Bool public let contentReader: String? - public let labels: [FeedItemLabel] + public var labels: [FeedItemLabel] public init( id: String, diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift new file mode 100644 index 000000000..fce103d6a --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift @@ -0,0 +1,57 @@ +import Combine +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func updateArticleLabelsPublisher(itemID: String, labelIDs: [String]) -> AnyPublisher<[FeedItemLabel], BasicError> { + enum MutationResult { + case saved(feedItem: [FeedItemLabel]) + case error(errorCode: Enums.SetLabelsErrorCode) + } + + let selection = Selection { + try $0.on( + setLabelsSuccess: .init { .saved(feedItem: try $0.labels(selection: feedItemLabelSelection.list)) }, + setLabelsError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) } + ) + } + + let mutation = Selection.Mutation { + try $0.setLabels( + input: InputObjects.SetLabelsInput( + linkId: itemID, + labelIds: labelIDs + ), + 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: graphqlError.first.debugDescription))) + } + + switch payload.data { + case let .saved(labels): + promise(.success(labels)) + case .error: + promise(.failure(.message(messageText: "failed to set labels"))) + } + case .failure: + promise(.failure(.message(messageText: "failed to set labels"))) + } + } + } + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } +} diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index ced09d0c4..55fef0e3b 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -164,8 +164,9 @@ public struct GridCard: View { if FeatureFlag.enableLabels { ScrollView(.horizontal, showsIndicators: false) { HStack { - TextChip(text: "label", color: .red) - TextChip(text: "longer label", color: .blue) + ForEach(item.labels, id: \.self) { + TextChip(feedItemLabel: $0) + } Spacer() } .frame(height: 30) diff --git a/apple/OmnivoreKit/Sources/Views/TextChip.swift b/apple/OmnivoreKit/Sources/Views/TextChip.swift index 775ad6854..b25bb7c94 100644 --- a/apple/OmnivoreKit/Sources/Views/TextChip.swift +++ b/apple/OmnivoreKit/Sources/Views/TextChip.swift @@ -1,11 +1,25 @@ +import Models import SwiftUI +import Utils + +public struct TextChip: View { + public init(text: String, color: Color) { + self.text = text + self.color = color + } + + public init?(feedItemLabel: FeedItemLabel) { + guard let color = Color(hex: feedItemLabel.color) else { return nil } + + self.text = feedItemLabel.name + self.color = color + } -struct TextChip: View { let text: String let color: Color let cornerRadius = 20.0 - var body: some View { + public var body: some View { Text(text) .padding(.horizontal, 10) .padding(.vertical, 5) From aec8059dbb52606c6e59e5787ca51968a013cd53 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 5 Apr 2022 14:26:38 -0700 Subject: [PATCH 13/14] remove alpha from color utils --- .../Sources/Utils/ColorUtils.swift | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift index 66d5f4bc2..7718fbf88 100644 --- a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift +++ b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift @@ -23,7 +23,6 @@ public extension Color { red = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 blue = CGFloat(rgb & 0x0000FF) / 255.0 - } else if length == 8 { red = CGFloat((rgb & 0xFF00_0000) >> 24) / 255.0 green = CGFloat((rgb & 0x00FF_0000) >> 16) / 255.0 @@ -53,27 +52,12 @@ public extension Color { let red = Float(components[0]) let green = Float(components[1]) let blue = Float(components[2]) - var alpha = Float(1.0) - if components.count >= 4 { - alpha = Float(components[3]) - } - - if alpha != Float(1.0) { - return String( - format: "%02lX%02lX%02lX%02lX", - lroundf(red * 255), - lroundf(green * 255), - lroundf(blue * 255), - lroundf(alpha * 255) - ) - } else { - return String( - format: "%02lX%02lX%02lX", - lroundf(red * 255), - lroundf(green * 255), - lroundf(blue * 255) - ) - } + return String( + format: "%02lX%02lX%02lX", + lroundf(red * 255), + lroundf(green * 255), + lroundf(blue * 255) + ) } } From 7ef4b383fbf4bab2744d19df7ca3d6abef5a869c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 5 Apr 2022 14:51:06 -0700 Subject: [PATCH 14/14] add ability to delete labels from ios --- .../App/Views/Profile/LabelsView.swift | 49 +++++++++++++++-- .../Mutations/RemoveLabelPublisher.swift | 53 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift index 9853cb993..852b64e8c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -46,20 +46,53 @@ final class LabelsViewModel: ObservableObject { ) .store(in: &subscriptions) } + + func deleteLabel(dataService: DataService, labelID: String) { + isLoading = true + + dataService.removeLabelPublisher(labelID: labelID).sink( + receiveCompletion: { [weak self] _ in + self?.isLoading = false + }, + receiveValue: { [weak self] _ in + self?.isLoading = false + self?.labels.removeAll { $0.id == labelID } + } + ) + .store(in: &subscriptions) + } } struct LabelsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = LabelsViewModel() + @State private var showDeleteConfirmation = false + @State private var labelToRemoveID: String? let footerText = "Use labels to create curated collections of links." var body: some View { Group { #if os(iOS) - Form { - innerBody + if #available(iOS 15.0, *) { + Form { + innerBody + .alert("Are you sure you want to delete this label?", isPresented: $showDeleteConfirmation) { + Button("Remove Link", role: .destructive) { + if let labelID = labelToRemoveID { + withAnimation { + viewModel.deleteLabel(dataService: dataService, labelID: labelID) + } + } + self.labelToRemoveID = nil + } + Button("Cancel", role: .cancel) { self.labelToRemoveID = nil } + } + } + } else { + Form { innerBody } } + #elseif os(macOS) List { innerBody @@ -89,7 +122,17 @@ struct LabelsView: View { if !viewModel.labels.isEmpty { Section(header: Text("Labels")) { ForEach(viewModel.labels, id: \.id) { label in - Text(label.name) + HStack { + Text(label.name) + Spacer() + Button( + action: { + labelToRemoveID = label.id + showDeleteConfirmation = true + }, + label: { Image(systemName: "trash") } + ) + } } } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift new file mode 100644 index 000000000..134954a4f --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift @@ -0,0 +1,53 @@ +import Combine +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func removeLabelPublisher(labelID: String) -> AnyPublisher { + enum MutationResult { + case success(labelID: String) + case error(errorCode: Enums.DeleteLabelErrorCode) + } + + let selection = Selection { + try $0.on( + deleteLabelSuccess: .init { + .success(labelID: try $0.label(selection: Selection.Label { try $0.id() })) + }, + deleteLabelError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) } + ) + } + + let mutation = Selection.Mutation { + try $0.deleteLabel(id: labelID, 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 payload.errors != nil { + promise(.failure(.message(messageText: "Error removing label"))) + } + + switch payload.data { + case .success: + promise(.success(true)) + case .error: + promise(.failure(.message(messageText: "Error removing label"))) + } + case .failure: + promise(.failure(.message(messageText: "Error removing label"))) + } + } + } + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } +}