From e17f243db0a737b2a5d562429aa0e6341c12c02a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 19 Dec 2023 09:59:40 +0800 Subject: [PATCH] Start using toast operation handler --- .../App/Views/Home/HomeFeedViewIOS.swift | 2 +- .../App/Views/Home/LibraryAddFeedView.swift | 7 +- .../App/Views/Home/LibraryScanFeedView.swift | 76 +++++-- .../App/Views/Profile/Subscriptions.swift | 212 +++++++++++------- .../Models/DataModels/Subscription.swift | 6 + .../Mutations/DeleteSubscription.swift | 4 +- .../Selections/SubsciptionSelection.swift | 2 + .../Contents.json | 38 ++++ 8 files changed, 245 insertions(+), 102 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/thSnackbarBackground.colorset/Contents.json diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 7ecbe4e11..4f4e7a230 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -732,7 +732,7 @@ struct AnimatingCellHeight: AnimatableModifier { NavigationView { LibraryAddFeedView(dismiss: { showAddFeedView = false - }) + }, toastOperationHandler: nil) } } .alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.", diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift index f39637e54..3ad59f93b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift @@ -10,6 +10,8 @@ struct LibraryAddFeedView: View { @State var newLinkURL: String = "" @EnvironmentObject var dataService: DataService + let toastOperationHandler: ToastOperationHandler? + enum FocusField: Hashable { case addLinkEditor } @@ -42,7 +44,10 @@ struct LibraryAddFeedView: View { } ToolbarItem(placement: .navigationBarTrailing) { NavigationLink( - destination: LibraryScanFeedView(dismiss: self.dismiss, viewModel: LibraryAddFeedViewModel(dataService: dataService, feedURL: newLinkURL)), + destination: LibraryScanFeedView( + dismiss: self.dismiss, + viewModel: LibraryAddFeedViewModel(dataService: dataService, feedURL: newLinkURL, toastOperationHandler: toastOperationHandler) + ), label: { Text("Add").bold() } ) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryScanFeedView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryScanFeedView.swift index 3c9849a3c..1d2bab136 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryScanFeedView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryScanFeedView.swift @@ -7,6 +7,7 @@ import Utils public class LibraryAddFeedViewModel: NSObject, ObservableObject { let dataService: DataService let feedURL: String + let toastOperationHandler: ToastOperationHandler? @Published var isLoading = true @Published var errorMessage: String = "" @@ -15,9 +16,10 @@ public class LibraryAddFeedViewModel: NSObject, ObservableObject { @Published var feeds: [Feed] = [] @Published var selected: [String] = [] - init(dataService: DataService, feedURL: String) { + init(dataService: DataService, feedURL: String, toastOperationHandler: ToastOperationHandler?) { self.dataService = dataService self.feedURL = feedURL + self.toastOperationHandler = toastOperationHandler } func scanFeed() async { @@ -38,28 +40,64 @@ public class LibraryAddFeedViewModel: NSObject, ObservableObject { } func addFeeds() async { - _ = await withTaskGroup(of: Bool.self) { group in - for feedURL in selected { - group.addTask { - (try? await self.dataService.subscribeToFeed(feedURL: feedURL)) ?? false + if let toastOperationHandler = toastOperationHandler { + toastOperationHandler.update(OperationStatus.isPerforming, "Subscribing...") + + let selected = self.selected + + let addTask = Task.detached(priority: .background) { + _ = await withTaskGroup(of: Bool.self) { group in + for feedURL in selected { + group.addTask { + (try? await self.dataService.subscribeToFeed(feedURL: feedURL)) ?? false + } + } + + var successCount = 0 + var failureCount = 0 + for await value in group { + if value { + successCount += 1 + } else { + failureCount += 1 + } + } + + let hasFailures = failureCount + DispatchQueue.main.async { + if hasFailures > 0 { + toastOperationHandler.update(OperationStatus.failure, "Failed to subscribe to \(hasFailures) feeds") + } else { + toastOperationHandler.update(OperationStatus.success, "Subscribed") + } + } } } - - var successCount = 0 - var failureCount = 0 - for await value in group { - if value { - successCount += 1 - } else { - failureCount += 1 + toastOperationHandler.performOperation(addTask) + } else { + _ = await withTaskGroup(of: Bool.self) { group in + for feedURL in selected { + group.addTask { + (try? await self.dataService.subscribeToFeed(feedURL: feedURL)) ?? false + } } - } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) { - if failureCount > 0 { - showInLibrarySnackbar("Failed to add \(failureCount) feeds") - } else { - showInLibrarySnackbar("Added \(successCount) feed\(successCount == 0 ? "" : "s")") + var successCount = 0 + var failureCount = 0 + for await value in group { + if value { + successCount += 1 + } else { + failureCount += 1 + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) { + if failureCount > 0 { + showInLibrarySnackbar("Failed to add \(failureCount) feeds") + } else { + showInLibrarySnackbar("Added \(successCount) feed\(successCount == 0 ? "" : "s")") + } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index ea7fec9bb..627975482 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -4,13 +4,21 @@ import SwiftUI import Transmission import Views -enum UnsubscribeState { +enum OperationStatus { case none - case isUnsubscribing - case unsubscribeSuccess - case unsubscribeFailure + case isPerforming + case success + case failure } +@MainActor +struct ToastOperationHandler { + let performOperation: (_: Sendable?) -> Void + let update: (_: OperationStatus, _: String) -> Void +} + +typealias OperationStatusHandler = (_: OperationStatus) -> Void + @MainActor final class SubscriptionsViewModel: ObservableObject { @Published var isLoading = true @Published var feeds = [Subscription]() @@ -18,7 +26,10 @@ enum UnsubscribeState { @Published var hasNetworkError = false @Published var subscriptionNameToCancel: String? @Published var presentingSubscription: Subscription? - @Published var unsubscribeState: UnsubscribeState = .none + + @Published var showOperationToast = false + @Published var operationStatus: OperationStatus = .none + @Published var operationMessage: String? func loadSubscriptions(dataService: DataService) async { isLoading = true @@ -34,56 +45,67 @@ enum UnsubscribeState { isLoading = false } - func cancelSubscription(dataService _: DataService, subscription: Subscription) async { - unsubscribeState = .isUnsubscribing - let subscriptionName = subscription.name + func cancelSubscription(dataService: DataService, subscription: Subscription) async { + operationMessage = "Unsubscribing..." + operationStatus = .isPerforming - // do { - // try await dataService.deleteSubscription(subscriptionName: subscriptionName) -// let index = subscriptions.firstIndex { $0.name == subscriptionName } -// if let index = index { -// subscriptions.remove(at: index) -// } -// unsubscribeState = .unsubscribeSuccess -// } catch { -// appLogger.debug("failed to remove subscription") -// unsubscribeState = .unsubscribeFailure -// } + do { + try await dataService.deleteSubscription(subscriptionName: subscription.name, subscriptionId: subscription.subscriptionID) + var list = subscription.type == .feed ? feeds : newsletters + let index = list.firstIndex { $0.subscriptionID == subscription.subscriptionID } + if let index = index { + list.remove(at: index) + switch subscription.type { + case .feed: + feeds = list + case .newsletter: + newsletters = list + } + } + operationMessage = "Unsubscribed" + operationStatus = .success + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) { + self.showOperationToast = false + } + } catch { + appLogger.debug("failed to remove subscription") + operationMessage = "Failed to unsubscribe" + operationStatus = .failure + } } } -struct UnsubscribeToast: View { +struct OperationToast: View { @ObservedObject var viewModel: SubscriptionsViewModel var body: some View { - HStack { - if viewModel.unsubscribeState == .isUnsubscribing { - Text("Unsubscribing...") - Spacer() - ProgressView() - } else if viewModel.unsubscribeState == .unsubscribeSuccess { - Text("You have been unsubscribed.") - Spacer() - Button(action: { - - }, label: { - Text("Done").bold() - }) - } else if viewModel.unsubscribeState == .unsubscribeFailure { - Text("There was an error unsubscribing") - Spacer() - Button(action: { - - }, label: { - Text("Done").bold() - }) + 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) } - .frame(minHeight: 50) - .frame(maxWidth: .infinity) - .padding(.bottom, 30) - .padding(.horizontal, 15) - .background(Color.systemBackground) + .padding(.bottom, 70) + .padding(.horizontal, 10) + .ignoresSafeArea(.all, edges: .bottom) } } @@ -93,13 +115,13 @@ struct SubscriptionsView: View { @State private var deleteConfirmationShown = false @State private var showDeleteCompleted = false + @State private var showAddFeedView = false @State private var showSubscriptionsSheet = false - @State private var showUnsubscribeToast = false var body: some View { Group { - WindowLink(level: .alert, transition: .move(edge: .bottom).combined(with: .opacity), isPresented: $showUnsubscribeToast) { - UnsubscribeToast(viewModel: viewModel) + WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) { + OperationToast(viewModel: viewModel) } label: { EmptyView() } @@ -133,6 +155,31 @@ struct SubscriptionsView: View { #endif } } + .sheet(isPresented: $showAddFeedView) { + let handler = ToastOperationHandler(performOperation: { sendable in + self.viewModel.showOperationToast = true + + Task { + _ = await sendable + viewModel.isLoading = true + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) { + Task { + await self.viewModel.loadSubscriptions(dataService: dataService) + self.viewModel.showOperationToast = false + } + } + } + }, update: { state, text in + viewModel.operationStatus = state + viewModel.operationMessage = text + }) + + NavigationView { + LibraryAddFeedView(dismiss: { + showAddFeedView = false + }, toastOperationHandler: handler) + } + } .formSheet(isPresented: $showSubscriptionsSheet) { if let presentingSubscription = viewModel.presentingSubscription { SubscriptionSettingsView( @@ -143,8 +190,8 @@ struct SubscriptionsView: View { unsubscribe: { subscription in showSubscriptionsSheet = false - viewModel.unsubscribeState = .isUnsubscribing - showUnsubscribeToast = true + viewModel.operationStatus = .isPerforming + viewModel.showOperationToast = true Task { await viewModel.cancelSubscription(dataService: dataService, subscription: subscription) } @@ -164,36 +211,43 @@ struct SubscriptionsView: View { private var innerBody: some View { Group { Section("Feeds") { - ForEach(viewModel.feeds, id: \.subscriptionID) { subscription in - Button(action: { - viewModel.presentingSubscription = subscription - showSubscriptionsSheet = true - }, label: { - SubscriptionCell(subscription: subscription) - }) + if viewModel.feeds.count <= 0, !viewModel.isLoading { + VStack(alignment: .center, spacing: 20) { + Text("You don't have any Feed items.") + .font(Font.system(size: 18, weight: .bold)) + + Text("Add an RSS/Atom feed") + .foregroundColor(Color.blue) + .onTapGesture { + showAddFeedView = true + } + } + .frame(minHeight: 80) + .frame(maxWidth: .infinity) + .padding() + } else { + ForEach(viewModel.feeds, id: \.subscriptionID) { subscription in + Button(action: { + viewModel.presentingSubscription = subscription + showSubscriptionsSheet = true + }, label: { + SubscriptionCell(subscription: subscription) + }) + } + } + } + if viewModel.newsletters.count > 0, !viewModel.isLoading { + Section("Newsletters") { + ForEach(viewModel.newsletters, id: \.subscriptionID) { subscription in + Button(action: { + viewModel.presentingSubscription = subscription + showSubscriptionsSheet = true + }, label: { + SubscriptionCell(subscription: subscription) + }) + } } } -// Section("Newsletters") { -// ForEach(viewModel.newsletters, id: \.subscriptionID) { subscription in -// SubscriptionCell(subscription: subscription) -// .swipeActions(edge: .trailing) { -// Button( -// role: .destructive, -// action: { -// deleteConfirmationShown = true -// viewModel.subscriptionNameToCancel = subscription.name -// }, -// label: { -// Image(systemName: "trash") -// } -// ) -// } -// .onTapGesture { -// expandedSubscription = subscription -// showSubscriptionsSheet = true -// } -// } -// } } .navigationTitle(LocalText.subscriptionsGeneric) } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift b/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift index 78a03fb59..2a8ff19df 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift @@ -7,6 +7,8 @@ public struct Subscription { public let subscriptionID: String public let name: String public let type: SubscriptionType + public let folder: String + public let fetchContent: Bool public let newsletterEmailAddress: String? public let status: SubscriptionStatus public let unsubscribeHttpUrl: String? @@ -21,6 +23,8 @@ public struct Subscription { subscriptionID: String, name: String, type: SubscriptionType, + folder: String, + fetchContent: Bool, newsletterEmailAddress: String?, status: SubscriptionStatus, unsubscribeHttpUrl: String?, @@ -34,6 +38,8 @@ public struct Subscription { self.subscriptionID = subscriptionID self.name = name self.type = type + self.folder = folder + self.fetchContent = fetchContent self.newsletterEmailAddress = newsletterEmailAddress self.status = status self.unsubscribeHttpUrl = unsubscribeHttpUrl diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift index e35a8bd69..497809b7e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift @@ -4,7 +4,7 @@ import Models import SwiftGraphQL public extension DataService { - func deleteSubscription(subscriptionName: String) async throws { + func deleteSubscription(subscriptionName: String, subscriptionId: String) async throws { enum MutationResult { case success(id: String) case error(errorMessage: String) @@ -20,7 +20,7 @@ public extension DataService { } let mutation = Selection.Mutation { - try $0.unsubscribe(name: subscriptionName, selection: selection) + try $0.unsubscribe(name: subscriptionName, subscriptionId: OptionalArgument(subscriptionId), selection: selection) } let path = appEnvironment.graphqlPath diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/SubsciptionSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/SubsciptionSelection.swift index 53944709a..7df09b519 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Selections/SubsciptionSelection.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/SubsciptionSelection.swift @@ -10,6 +10,8 @@ let subscriptionSelection = Selection.Subscription { subscriptionID: try $0.id(), name: try $0.name(), type: try SubscriptionType.from($0.type()), + folder: try $0.folder() ?? "inbox", + fetchContent: try $0.fetchContent() ?? true, newsletterEmailAddress: try $0.newsletterEmail(), status: try SubscriptionStatus.make(from: $0.status()), unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(), diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/thSnackbarBackground.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/thSnackbarBackground.colorset/Contents.json new file mode 100644 index 000000000..982111ca3 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/thSnackbarBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2A", + "green" : "0x2A", + "red" : "0x2A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +}