diff --git a/apple/OmnivoreKit/Sources/App/Views/BottomBars/CustomToolBar.swift b/apple/OmnivoreKit/Sources/App/Views/BottomBars/CustomToolBar.swift index d9966181f..6cc415cee 100644 --- a/apple/OmnivoreKit/Sources/App/Views/BottomBars/CustomToolBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/BottomBars/CustomToolBar.swift @@ -3,7 +3,9 @@ import SwiftUI import Views struct CustomToolBar: View { + let isFollowing: Bool let isArchived: Bool + let moveToInboxAction: () -> Void let archiveAction: () -> Void let unarchiveAction: () -> Void let shareAction: () -> Void @@ -30,7 +32,9 @@ struct CustomToolBar: View { .frame(height: 0.5) .frame(maxWidth: .infinity) HStack(spacing: 0) { - if isArchived { + if isFollowing { + ToolBarButton(image: Image.tabLibrary, action: moveToInboxAction) + } else if isArchived { ToolBarButton(image: Image.toolbarUnarchive, action: unarchiveAction) } else { ToolBarButton(image: Image.toolbarArchive, action: archiveAction) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 96c859699..23575651c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -57,7 +57,7 @@ struct LabelsView: View { let trimmedLabelName = viewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines) Image(systemName: "tag").foregroundColor(.blue) Text( - viewModel.labelSearchFilter.count > 0 ? + viewModel.labelSearchFilter.count > 0 && viewModel.labelSearchFilter != ZWSP ? "Create: \"\(trimmedLabelName)\" label" : LocalText.createLabelMessage ).foregroundColor(.blue) @@ -106,16 +106,6 @@ struct CreateLabelView: View { var innerBody: some View { VStack { - HStack { - if !newLabelName.isEmpty, newLabelColor != .clear { - TextChip(text: newLabelName, color: newLabelColor) - } else { - Text(LocalText.labelsViewAssignNameColor).font(.appBody) - } - Spacer() - } - .padding(.bottom, 8) - TextField(LocalText.labelNamePlaceholder, text: $newLabelName) .textFieldStyle(StandardTextFieldStyle()) .onChange(of: newLabelName) { inputLabelName in diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift index cdbff89f0..4f49e17f8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/NewsletterEmailsView.swift @@ -2,19 +2,24 @@ import Models import PopupView import Services import SwiftUI +import Transmission import Views @MainActor final class NewsletterEmailsViewModel: ObservableObject { @Published var isLoading = false + @Published var showAddressCopied = false @Published var emails = [NewsletterEmail]() func loadEmails(dataService: DataService) async { isLoading = true - if let objectIDs = try? await dataService.newsletterEmails() { + do { + let objectIDs = try await dataService.newsletterEmails() await dataService.viewContext.perform { [weak self] in self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } } + } catch { + print("ERROR LOADING EMAILS: ", error) } isLoading = false @@ -39,16 +44,17 @@ struct NewsletterEmailsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = NewsletterEmailsViewModel() - @State var showSnackbar = false + @State var showAddressCopied = false @State var snackbarOperation: SnackbarOperation? - func snackbar(message: String) { - snackbarOperation = SnackbarOperation(message: message, undoAction: nil) - showSnackbar = true - } - var body: some View { Group { + WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showAddressCopied) { + MessageToast() + } label: { + EmptyView() + } + #if os(iOS) Form { innerBody @@ -61,20 +67,6 @@ struct NewsletterEmailsView: View { #endif } .task { await viewModel.loadEmails(dataService: dataService) } - .popup(isPresented: $showSnackbar) { - if let operation = snackbarOperation { - Snackbar(isShowing: $showSnackbar, operation: operation) - } else { - EmptyView() - } - } customize: { - $0 - .type(.toast) - .autohideIn(2) - .position(.bottom) - .animation(.spring()) - .closeOnTapOutside(true) - } } private var innerBody: some View { @@ -97,28 +89,77 @@ struct NewsletterEmailsView: View { if !viewModel.emails.isEmpty { Section(header: Text(LocalText.newsletterEmailsExisting)) { - ForEach(viewModel.emails) { newsletterEmail in - Button( - action: { - #if os(iOS) - UIPasteboard.general.string = newsletterEmail.email - #endif - - #if os(macOS) - let pasteBoard = NSPasteboard.general - pasteBoard.clearContents() - pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString]) - #endif - - snackbar(message: "Email copied") - }, - label: { Text(newsletterEmail.unwrappedEmail) } - ) + ForEach(viewModel.emails) { email in + NewsletterEmailRow(viewModel: viewModel, email: email, folderSelection: email.folder) } } } } - .navigationTitle(LocalText.emailsGeneric) } } + +struct NewsletterEmailRow: View { + @StateObject var viewModel: NewsletterEmailsViewModel + @State var email: NewsletterEmail + @State var folderSelection: String? + + var body: some View { + VStack { + Button( + action: { + #if os(iOS) + UIPasteboard.general.string = email.email + #endif + + #if os(macOS) + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString]) + #endif + + viewModel.showAddressCopied = true + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) { + viewModel.showAddressCopied = false + } + }, + label: { Text(email.unwrappedEmail).font(Font.appFootnote) } + ) + Divider() + Picker("Destination Folder", selection: $folderSelection) { + Text("Inbox").tag("inbox") + Text("Following").tag("following") + } + .pickerStyle(MenuPickerStyle()) + .onChange(of: folderSelection) { _ in +// Task { +// viewModel.showOperationToast = true +// await viewModel.updateSubscription(dataService: dataService, subscription: subscription, folder: newValue) +// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) { +// viewModel.showOperationToast = false +// } +// } + } + } + } +} + +struct MessageToast: View { + var body: some View { + VStack { + HStack { + Text("Address copied") + Spacer() + } + .padding(10) + .frame(minHeight: 50) + .frame(maxWidth: .infinity) + .background(Color(hex: "2A2A2A")) + .cornerRadius(4.0) + .tint(Color.green) + } + .padding(.bottom, 70) + .padding(.horizontal, 10) + .ignoresSafeArea(.all, edges: .bottom) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift similarity index 88% rename from apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift rename to apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift index 6afca2f24..d40547fe6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift @@ -73,6 +73,19 @@ typealias OperationStatusHandler = (_: OperationStatus) -> Void operationStatus = .failure } } + + func updateSubscription(dataService: DataService, subscription: Subscription, folder: String? = nil, fetchContent: Bool? = nil) async { + operationMessage = "Updating subscription..." + operationStatus = .isPerforming + do { + try await dataService.updateSubscription(subscription.subscriptionID, folder: folder, fetchContent: fetchContent) + operationMessage = "Subscription updated" + operationStatus = .success + } catch { + operationMessage = "Failed to update subscription" + operationStatus = .failure + } + } } struct OperationToast: View { @@ -186,6 +199,8 @@ struct SubscriptionsView: View { subscription: presentingSubscription, viewModel: viewModel, dataService: dataService, + prefetchContent: presentingSubscription.fetchContent, + folderSelection: presentingSubscription.folder, dismiss: { showSubscriptionsSheet = false }, unsubscribe: { subscription in showSubscriptionsSheet = false @@ -365,13 +380,24 @@ struct SubscriptionSettingsView: View { Text("Inbox").tag("inbox") Text("Following").tag("following") } - .pickerStyle(MenuPickerStyle()) // makes the picker appear as a menu - .onAppear { - folderSelection = subscription.folder - print("FOLDER: ", folderSelection) + .pickerStyle(MenuPickerStyle()) + .onChange(of: folderSelection) { newValue in + Task { + viewModel.showOperationToast = true + await viewModel.updateSubscription(dataService: dataService, subscription: subscription, folder: newValue) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) { + viewModel.showOperationToast = false + } + } } - .onChange(of: folderSelection) { _ in - print("CHANGED FOLDER: ", folderSelection) + .onChange(of: prefetchContent) { newValue in + Task { + viewModel.showOperationToast = true + await viewModel.updateSubscription(dataService: dataService, subscription: subscription, fetchContent: newValue) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) { + viewModel.showOperationToast = false + } + } } } }.listStyle(.insetGrouped) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index a5d95f026..8bbe955fa 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -3,6 +3,7 @@ import Models import PopupView import Services import SwiftUI +import Transmission import Utils import Views import WebKit @@ -367,6 +368,11 @@ struct WebReaderContainerView: View { var body: some View { ZStack { + WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) { + ReaderOperationToast(viewModel: viewModel) + } label: { + EmptyView() + } if let articleContent = viewModel.articleContent { WebReader( item: item, @@ -568,7 +574,9 @@ struct WebReaderContainerView: View { } if navBarVisible { CustomToolBar( + isFollowing: item.folder == "following", isArchived: item.isArchived, + moveToInboxAction: moveToInbox, archiveAction: archive, unarchiveAction: archive, shareAction: share, @@ -615,6 +623,25 @@ struct WebReaderContainerView: View { } } + func moveToInbox() { + Task { + viewModel.showOperationToast = true + viewModel.operationMessage = "Moving to library..." + viewModel.operationStatus = .isPerforming + do { + try await dataService.moveItem(itemID: item.unwrappedID, folder: "inbox") + viewModel.operationMessage = "Moved to library" + viewModel.operationStatus = .success + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) { + viewModel.showOperationToast = false + } + } catch { + viewModel.operationMessage = "Error moving" + viewModel.operationStatus = .failure + } + } + } + func archive() { let isArchived = item.isArchived dataService.archiveLink(objectID: item.objectID, archived: !isArchived) @@ -670,3 +697,37 @@ struct WebReaderContainerView: View { openURL(url) } } + +struct ReaderOperationToast: View { + @State var viewModel: WebReaderViewModel + + var body: some View { + VStack { + HStack { + if viewModel.operationStatus == .isPerforming { + Text(viewModel.operationMessage ?? "Performing...") + Spacer() + ProgressView() + } else if viewModel.operationStatus == .success { + Text(viewModel.operationMessage ?? "Success") + Spacer() + } else if viewModel.operationStatus == .failure { + Text(viewModel.operationMessage ?? "Failure") + Spacer() + Button(action: { viewModel.showOperationToast = false }, label: { + Text("Done").bold() + }) + } + } + .padding(10) + .frame(minHeight: 50) + .frame(maxWidth: .infinity) + .background(Color(hex: "2A2A2A")) + .cornerRadius(4.0) + .tint(Color.green) + } + .padding(.bottom, 60) + .padding(.horizontal, 10) + .ignoresSafeArea(.all, edges: .bottom) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 8301b90c0..968594f04 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -16,6 +16,10 @@ struct SafariWebLink: Identifiable { @Published var isDownloadingAudio: Bool = false @Published var audioDownloadTask: Task? + @Published var operationMessage: String? + @Published var showOperationToast: Bool = false + @Published var operationStatus: OperationStatus = .none + @Published var showSnackbar: Bool = false var snackbarOperation: SnackbarOperation? diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index d8adbc203..6efa8faa3 100644 --- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -98,6 +98,7 @@ + diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift index f65f5400f..7bb34797d 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateNewsletterEmailMutation.swift @@ -20,6 +20,7 @@ public extension DataService { InternalNewsletterEmail( emailId: try $0.id(), email: try $0.address(), + folder: try $0.folder(), confirmationCode: try $0.confirmationCode() ) })) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateSubscription.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateSubscription.swift new file mode 100644 index 000000000..1c16479b1 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateSubscription.swift @@ -0,0 +1,50 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func updateSubscription(_ subscriptionID: String, folder: String? = nil, fetchContent: Bool? = nil) async throws { + enum MutationResult { + case success(subscriptionID: String) + case error(errorMessage: String) + } + + let selection = Selection { + try $0.on( + updateSubscriptionError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, + updateSubscriptionSuccess: .init { .success(subscriptionID: try $0.subscription(selection: subscriptionSelection).subscriptionID) } + ) + } + + let mutation = Selection.Mutation { + try $0.updateSubscription( + input: InputObjects.UpdateSubscriptionInput( + fetchContent: OptionalArgument(fetchContent), + folder: OptionalArgument(folder), + id: subscriptionID + ), + selection: selection + ) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network error")) + return + } + + switch payload.data { + case .success: + continuation.resume() + case let .error(errorMessage: errorMessage): + continuation.resume(throwing: BasicError.message(messageText: errorMessage)) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift index ff8e05bf2..34c98d69f 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/NewsletterEmailsQuery.swift @@ -14,6 +14,7 @@ public extension DataService { InternalNewsletterEmail( emailId: try $0.id(), email: try $0.address(), + folder: try $0.folder(), confirmationCode: try $0.confirmationCode() ) } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift index 7620bec8e..a8ef579f7 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalNewsletterEmail.swift @@ -5,6 +5,7 @@ import Models struct InternalNewsletterEmail { let emailId: String let email: String + let folder: String let confirmationCode: String? func persist(context: NSManagedObjectContext) -> NSManagedObjectID? { @@ -32,6 +33,7 @@ struct InternalNewsletterEmail { newsletterEmail.emailId = emailId newsletterEmail.email = email + newsletterEmail.folder = folder newsletterEmail.confirmationCode = confirmationCode return newsletterEmail }