diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index d6a807959..3dc9f8e0e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -11,9 +11,16 @@ struct FeedCardNavigationLink: View { @ObservedObject var viewModel: HomeFeedViewModel var body: some View { - ZStack { + let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)) + #if os(iOS) + let modifiedDestination = destination.navigationBarHidden(true) + #else + let modifiedDestination = destination + #endif + + return ZStack { NavigationLink( - destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)), + destination: modifiedDestination, tag: item, selection: $viewModel.selectedLinkItem ) { @@ -42,9 +49,16 @@ struct GridCardNavigationLink: View { @ObservedObject var viewModel: HomeFeedViewModel var body: some View { - ZStack { + let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)) + #if os(iOS) + let modifiedDestination = destination.navigationBarHidden(true) + #else + let modifiedDestination = destination + #endif + + return ZStack { NavigationLink( - destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)), + destination: modifiedDestination, tag: item, selection: $viewModel.selectedLinkItem ) { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index f185006e8..7c354f05c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -14,6 +14,10 @@ import Views @ObservedObject var viewModel: HomeFeedViewModel + func loadItems(isRefresh: Bool) { + Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) } + } + var body: some View { List { Section { @@ -23,8 +27,18 @@ import Views viewModel: viewModel ) .contextMenu { + Button( + action: { viewModel.itemUnderLabelEdit = item }, + label: { Label("Edit Labels", systemImage: "tag") } + ) Button(action: { - viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: !item.isArchived) + withAnimation(.linear(duration: 0.4)) { + viewModel.setLinkArchived( + dataService: dataService, + objectID: item.objectID, + archived: !item.isArchived + ) + } }, label: { Label( item.isArchived ? "Unarchive" : "Archive", @@ -51,7 +65,7 @@ import Views Button("Remove Link", role: .destructive) { if let itemToRemove = itemToRemove { withAnimation { - viewModel.removeLink(dataService: dataService, linkId: itemToRemove.id) + viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID) self.itemToRemove = nil } } @@ -81,16 +95,16 @@ import Views .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without - viewModel.loadItems(dataService: dataService, isRefresh: true) + loadItems(isRefresh: true) } .onSubmit(of: .search) { - viewModel.loadItems(dataService: dataService, isRefresh: true) + loadItems(isRefresh: true) } .toolbar { ToolbarItem { Button( action: { - viewModel.loadItems(dataService: dataService, isRefresh: true) + loadItems(isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) @@ -105,7 +119,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, isRefresh: true) + loadItems(isRefresh: true) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 876564b45..f478035a8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -140,10 +140,8 @@ struct LinkItemDetailView: View { #if os(iOS) if viewModel.item.isPDF { fixedNavBarReader - } else if FeatureFlag.useLocalWebView { - WebReaderContainerView(item: viewModel.item, homeFeedViewModel: viewModel.homeFeedViewModel) } else { - hidingNavBarReader + WebReaderContainerView(item: viewModel.item, homeFeedViewModel: viewModel.homeFeedViewModel) } #else fixedNavBarReader @@ -250,9 +248,7 @@ struct LinkItemDetailView: View { navBar Spacer() } - .navigationBarHidden(true) } - } else { VStack(spacing: 0) { navBar @@ -264,7 +260,6 @@ struct LinkItemDetailView: View { rawAuthCookie: authenticator.omnivoreAuthCookieString ) } - .navigationBarHidden(true) } } #endif diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift index ae9660ffc..c9f693d73 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -1,49 +1,36 @@ -import Combine import Models import Services import SwiftUI import Views -final class NewsletterEmailsViewModel: ObservableObject { - private var hasLoadedInitialEmails = false +@MainActor final class NewsletterEmailsViewModel: ObservableObject { @Published var isLoading = false @Published var emails = [NewsletterEmail]() - var subscriptions = Set() - - func loadEmails(dataService: DataService) { + func loadEmails(dataService: DataService) async { isLoading = true - dataService.newsletterEmailsPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] objectIDs in - self?.isLoading = false - dataService.viewContext.perform { - self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } - } - self?.hasLoadedInitialEmails = true + if let objectIDs = try? await dataService.newsletterEmails() { + await dataService.viewContext.perform { [weak self] in + self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } } - ) - .store(in: &subscriptions) + } + + isLoading = false } - func createEmail(dataService: DataService) { + func createEmail(dataService: DataService) async { isLoading = true - dataService.createNewsletterEmailPublisher().sink( - receiveCompletion: { [weak self] _ in - self?.isLoading = false - }, - receiveValue: { [weak self] objectID in - self?.isLoading = false - dataService.viewContext.perform { - if let item = dataService.viewContext.object(with: objectID) as? NewsletterEmail { - self?.emails.insert(item, at: 0) - } + if let objectID = try? await dataService.createNewsletter() { + await dataService.viewContext.perform { [weak self] in + if let item = dataService.viewContext.object(with: objectID) as? NewsletterEmail { + self?.emails.insert(item, at: 0) } } - ) - .store(in: &subscriptions) + } + + isLoading = false } } @@ -65,7 +52,7 @@ struct NewsletterEmailsView: View { .listStyle(InsetListStyle()) #endif } - .onAppear { viewModel.loadEmails(dataService: dataService) } + .task { await viewModel.loadEmails(dataService: dataService) } } private var innerBody: some View { @@ -73,7 +60,7 @@ struct NewsletterEmailsView: View { Section(footer: Text(footerText)) { Button( action: { - viewModel.createEmail(dataService: dataService) + Task { await viewModel.createEmail(dataService: dataService) } }, label: { HStack { @@ -98,7 +85,7 @@ struct NewsletterEmailsView: View { #if os(macOS) let pasteBoard = NSPasteboard.general pasteBoard.clearContents() - pasteBoard.writeObjects([newsletterEmail.email as NSString]) + pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString]) #endif Snackbar.show(message: "Email copied") diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 07d967767..68071010a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -72,10 +72,8 @@ struct ProfileView: View { } Section { - if FeatureFlag.enableLabels { - NavigationLink(destination: LabelsView()) { - Text("Labels") - } + NavigationLink(destination: LabelsView()) { + Text("Labels") } NavigationLink(destination: NewsletterEmailsView()) { diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 67a45339b..ebd821451 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -222,13 +222,10 @@ import WebKit navBar Spacer() } - .navigationBarHidden(true) - }.onDisappear { // Clear the shared webview content when exiting WebViewManager.shared().loadHTMLString("", baseURL: nil) } - .navigationBarHidden(true) } } #endif diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/NewsletterEmail.swift b/apple/OmnivoreKit/Sources/Models/DataModels/NewsletterEmail.swift index 5a7f540ba..825d6cace 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/NewsletterEmail.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/NewsletterEmail.swift @@ -1,11 +1,23 @@ +import CoreData import Foundation public extension NewsletterEmail { - var unwrappedEmailId: String { - emailId ?? "" - } + var unwrappedEmailId: String { emailId ?? "" } - var unwrappedEmail: String { - email ?? "" + var unwrappedEmail: String { email ?? "" } + + static func lookup(byID emailID: String, inContext context: NSManagedObjectContext) -> NewsletterEmail? { + let fetchRequest: NSFetchRequest = NewsletterEmail.fetchRequest() + fetchRequest.predicate = NSPredicate( + format: "%K == %@", #keyPath(NewsletterEmail.emailId), emailID + ) + + var email: NewsletterEmail? + + context.performAndWait { + email = (try? context.fetch(fetchRequest))?.first + } + + return email } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift index 4eda0576f..454a76c46 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift @@ -5,7 +5,7 @@ import Models import SwiftGraphQL public extension DataService { - func createNewsletterEmailPublisher() -> AnyPublisher { + func createNewsletter() async throws -> NSManagedObjectID { enum MutationResult { case saved(newsletterEmail: InternalNewsletterEmail) case error(errorCode: Enums.CreateNewsletterEmailErrorCode) @@ -34,32 +34,24 @@ public extension DataService { 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)"))) - } + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { [weak self] queryResult in + guard let payload = try? queryResult.get(), let self = self else { + continuation.resume(throwing: BasicError.message(messageText: "network error")) + return + } - switch payload.data { - case let .saved(newsletterEmail: newsletterEmail): - if let newsletterEmailObjectID = newsletterEmail.persist(context: self.backgroundContext) { - promise(.success(newsletterEmailObjectID)) - } else { - promise(.failure(.message(messageText: "coredata error"))) - } - case let .error(errorCode: errorCode): - promise(.failure(.message(messageText: errorCode.rawValue))) - } - case .failure: - promise(.failure(.message(messageText: "graphql error"))) + switch payload.data { + case let .saved(newsletterEmail: newsletterEmail): + if let newsletterEmailObjectID = newsletterEmail.persist(context: self.backgroundContext) { + continuation.resume(returning: newsletterEmailObjectID) + } else { + continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) } + case let .error(errorCode: errorCode): + continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue)) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift index 1465c15e0..49545956c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift @@ -1,11 +1,10 @@ -import Combine import CoreData import Foundation import Models import SwiftGraphQL public extension DataService { - func newsletterEmailsPublisher() -> AnyPublisher<[NSManagedObjectID], ServerError> { + func newsletterEmails() async throws -> [NSManagedObjectID] { enum QueryResult { case success(result: [InternalNewsletterEmail]) case error(error: String) @@ -36,29 +35,26 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders + let context = backgroundContext - 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): - if let newsletterEmailObjectIDs = result.persist(context: self.backgroundContext) { - promise(.success(newsletterEmailObjectIDs)) - } else { - promise(.failure(.unknown)) - } - case .error: - promise(.failure(.unknown)) - } - case .failure: - promise(.failure(.unknown)) + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network request failed")) + return + } + + switch payload.data { + case let .success(result: result): + if let newsletterEmailObjectIDs = result.persist(context: context) { + continuation.resume(returning: newsletterEmailObjectIDs) + } else { + continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) } + case .error: + continuation.resume(throwing: BasicError.message(messageText: "Newsletter Email fetch error")) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift index 7b2ad3e58..7620bec8e 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift @@ -27,7 +27,9 @@ struct InternalNewsletterEmail { } func asManagedObject(inContext context: NSManagedObjectContext) -> NewsletterEmail { - let newsletterEmail = NewsletterEmail(entity: NewsletterEmail.entity(), insertInto: context) + let existingEmail = NewsletterEmail.lookup(byID: emailId, inContext: context) + let newsletterEmail = existingEmail ?? NewsletterEmail(entity: NewsletterEmail.entity(), insertInto: context) + newsletterEmail.emailId = emailId newsletterEmail.email = email newsletterEmail.confirmationCode = confirmationCode diff --git a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift index ab5196f5a..43385fcc4 100644 --- a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift +++ b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift @@ -14,6 +14,4 @@ public enum FeatureFlag { public static let enablePushNotifications = false public static let enableShareButton = false public static let enableSnooze = false - public static let enableLabels = true - public static let useLocalWebView = true } diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index c52a5f2fb..b239e62e4 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -149,20 +149,16 @@ public struct GridCard: View { .onTapGesture { tapHandler() } // Category Labels - if FeatureFlag.enableLabels { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - ForEach(item.labels.asArray(of: LinkedItemLabel.self), id: \.self) { - TextChip(feedItemLabel: $0) - } - Spacer() + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(item.labels.asArray(of: LinkedItemLabel.self), id: \.self) { + TextChip(feedItemLabel: $0) } - .frame(height: 30) - .padding(.horizontal) - .padding(.bottom, 8) + Spacer() } - } else { - Spacer(minLength: 8) + .frame(height: 30) + .padding(.horizontal) + .padding(.bottom, 8) } } .background(