Better unsubscribe view

This commit is contained in:
Jackson Harper
2023-12-18 17:35:08 +08:00
parent e312562636
commit 4291c1149b
3 changed files with 242 additions and 78 deletions

View File

@ -1,4 +1,5 @@
import Foundation
import Models
import Services
import SwiftUI
@ -25,7 +26,7 @@ public struct LibrarySplitView: View {
hasFeatureCards: false,
hasReadNowSection: false,
leadingSwipeActions: [.moveToInbox],
trailingSwipeActions: [.archive, .delete],
trailingSwipeActions: [.delete],
cardStyle: .library
)
)
@ -49,6 +50,27 @@ public struct LibrarySplitView: View {
$0.preferredPrimaryColumnWidth = 230
$0.displayModeButtonVisibility = .always
}
.onOpenURL { url in
inboxViewModel.linkRequest = nil
if let deepLink = DeepLink.make(from: url) {
switch deepLink {
case let .search(query):
inboxViewModel.searchTerm = query
case let .savedSearch(named):
if let filter = inboxViewModel.findFilter(dataService, named: named) {
inboxViewModel.appliedFilter = filter
}
case let .webAppLinkRequest(requestID):
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
withoutAnimation {
inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
inboxViewModel.presentWebContainer = true
}
}
}
}
// selectedTab = "inbox"
}
.onReceive(NSNotification.performSyncPublisher) { _ in
Task {
await syncManager.syncItems(dataService: dataService)

View File

@ -29,7 +29,7 @@ struct LibraryTabView: View {
UITabBar.appearance().isHidden = true
}
@StateObject private var libraryViewModel = HomeFeedViewModel(
@StateObject private var inboxViewModel = HomeFeedViewModel(
folder: "inbox",
fetcher: LibraryItemFetcher(),
listConfig: LibraryListConfig(
@ -48,7 +48,7 @@ struct LibraryTabView: View {
hasFeatureCards: false,
hasReadNowSection: false,
leadingSwipeActions: [.moveToInbox],
trailingSwipeActions: [.archive, .delete],
trailingSwipeActions: [.delete],
cardStyle: .library
)
)
@ -67,7 +67,7 @@ struct LibraryTabView: View {
}
NavigationView {
HomeFeedContainerView(viewModel: libraryViewModel)
HomeFeedContainerView(viewModel: inboxViewModel)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
}.tag("inbox")
@ -100,20 +100,20 @@ struct LibraryTabView: View {
}
}
.onOpenURL { url in
libraryViewModel.linkRequest = nil
inboxViewModel.linkRequest = nil
if let deepLink = DeepLink.make(from: url) {
switch deepLink {
case let .search(query):
libraryViewModel.searchTerm = query
inboxViewModel.searchTerm = query
case let .savedSearch(named):
if let filter = libraryViewModel.findFilter(dataService, named: named) {
libraryViewModel.appliedFilter = filter
if let filter = inboxViewModel.findFilter(dataService, named: named) {
inboxViewModel.appliedFilter = filter
}
case let .webAppLinkRequest(requestID):
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
withoutAnimation {
libraryViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
libraryViewModel.presentWebContainer = true
inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
inboxViewModel.presentWebContainer = true
}
}
}

View File

@ -1,14 +1,24 @@
import Models
import Services
import SwiftUI
import Transmission
import Views
enum UnsubscribeState {
case none
case isUnsubscribing
case unsubscribeSuccess
case unsubscribeFailure
}
@MainActor final class SubscriptionsViewModel: ObservableObject {
@Published var isLoading = true
@Published var feeds = [Subscription]()
@Published var newsletters = [Subscription]()
@Published var hasNetworkError = false
@Published var subscriptionNameToCancel: String?
@Published var presentingSubscription: Subscription?
@Published var unsubscribeState: UnsubscribeState = .none
func loadSubscriptions(dataService: DataService) async {
isLoading = true
@ -24,20 +34,56 @@ import Views
isLoading = false
}
func cancelSubscription(dataService: DataService) async -> Bool {
guard let subscriptionName = subscriptionNameToCancel else { return false }
func cancelSubscription(dataService _: DataService, subscription: Subscription) async {
unsubscribeState = .isUnsubscribing
let subscriptionName = subscription.name
do {
try await dataService.deleteSubscription(subscriptionName: subscriptionName)
// do {
// try await dataService.deleteSubscription(subscriptionName: subscriptionName)
// let index = subscriptions.firstIndex { $0.name == subscriptionName }
// if let index = index {
// subscriptions.remove(at: index)
// }
return true
} catch {
appLogger.debug("failed to remove subscription")
return false
// unsubscribeState = .unsubscribeSuccess
// } catch {
// appLogger.debug("failed to remove subscription")
// unsubscribeState = .unsubscribeFailure
// }
}
}
struct UnsubscribeToast: 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()
})
}
}
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.padding(.bottom, 30)
.padding(.horizontal, 15)
.background(Color.systemBackground)
}
}
@ -45,10 +91,18 @@ struct SubscriptionsView: View {
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = SubscriptionsViewModel()
@State private var deleteConfirmationShown = false
@State private var progressViewOpacity = 0.0
@State private var showDeleteCompleted = 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)
} label: {
EmptyView()
}
if viewModel.isLoading {
ProgressView()
} else if viewModel.hasNetworkError {
@ -79,6 +133,25 @@ struct SubscriptionsView: View {
#endif
}
}
.formSheet(isPresented: $showSubscriptionsSheet) {
if let presentingSubscription = viewModel.presentingSubscription {
SubscriptionSettingsView(
subscription: presentingSubscription,
viewModel: viewModel,
dataService: dataService,
dismiss: { showSubscriptionsSheet = false },
unsubscribe: { subscription in
showSubscriptionsSheet = false
viewModel.unsubscribeState = .isUnsubscribing
showUnsubscribeToast = true
Task {
await viewModel.cancelSubscription(dataService: dataService, subscription: subscription)
}
}
)
}
}
.task {
await viewModel.loadSubscriptions(dataService: dataService)
}
@ -92,59 +165,72 @@ struct SubscriptionsView: View {
Group {
Section("Feeds") {
ForEach(viewModel.feeds, id: \.subscriptionID) { subscription in
SubscriptionCell(subscription: subscription)
.swipeActions(edge: .trailing) {
Button(
role: .destructive,
action: {
deleteConfirmationShown = true
viewModel.subscriptionNameToCancel = subscription.name
},
label: {
Image(systemName: "trash")
}
)
}
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")
}
)
}
}
}
}
.alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) {
Button("Yes", role: .destructive) {
Task {
let unsubscribed = await viewModel.cancelSubscription(dataService: dataService)
// Snackbar.show(message: unsubscribed ? "Subscription cancelled." : "Could not unsubscribe.")
}
}
Button("No", role: .cancel) {
viewModel.subscriptionNameToCancel = nil
}
// 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)
}
}
struct SubscriptionCell: View {
struct SubscriptionRow<Content: View>: View {
let subscription: Subscription
let useImageSpacer: Bool
@ViewBuilder let trailingButton: Content
var body: some View {
HStack {
Group {
if let icon = subscription.icon, let imageURL = URL(string: icon) {
AsyncImage(url: imageURL) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 40, height: 40)
.cornerRadius(6)
} else if phase.error != nil {
Color.clear.frame(width: 40, height: 40, alignment: .top)
} else {
Color.clear
.frame(width: 40, height: 40)
.cornerRadius(2)
}
}
} else if useImageSpacer {
Color.clear
.frame(width: 40, height: 40)
.cornerRadius(2)
}
}.padding(.trailing, 10)
VStack(alignment: .leading, spacing: 6) {
Text(subscription.name)
.font(.appCallout)
@ -164,25 +250,81 @@ struct SubscriptionCell: View {
Spacer()
Group {
if let icon = subscription.icon, let imageURL = URL(string: icon) {
AsyncImage(url: imageURL) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 40, height: 40)
.cornerRadius(6)
} else if phase.error != nil {
EmptyView().frame(width: 40, height: 40, alignment: .top)
} else {
Color.appButtonBackground
.frame(width: 40, height: 40)
.cornerRadius(2)
}
trailingButton
}.frame(minHeight: 50)
}
}
struct SubscriptionCell: View {
let subscription: Subscription
var body: some View {
SubscriptionRow(subscription: subscription, useImageSpacer: true, trailingButton: {
Image(systemName: "ellipsis")
})
}
}
struct SubscriptionSettingsView: View {
let subscription: Subscription
let viewModel: SubscriptionsViewModel
let dataService: DataService
@State var prefetchContent = false
@State var deleteConfirmationShown = false
@State var showDeleteCompleted = false
let dismiss: () -> Void
let unsubscribe: (_: Subscription) -> Void
var body: some View {
VStack {
SubscriptionRow(subscription: subscription, useImageSpacer: false, trailingButton: {
Button(action: {
dismiss()
}, label: {
ZStack {
Circle()
.foregroundColor(Color.circleButtonBackground)
.frame(width: 30, height: 30)
Image(systemName: "xmark")
.resizable(resizingMode: Image.ResizingMode.stretch)
.foregroundColor(Color.circleButtonForeground)
.aspectRatio(contentMode: .fit)
.font(Font.title.weight(.bold))
.frame(width: 12, height: 12)
}
})
})
.padding(.top, 15)
.padding(.horizontal, 15)
List {
Toggle(isOn: $prefetchContent, label: { Text("Prefetch Content:").foregroundColor(.appGrayText) })
HStack {
Text("Destination Folder:")
.foregroundColor(.appGrayText)
Spacer()
Menu(content: {}, label: {
Text("Following <>")
})
}
}.frame(minHeight: 50)
}.listStyle(.insetGrouped)
Spacer()
Button("Unsubscribe", role: .destructive) { deleteConfirmationShown = true }
.frame(maxWidth: .infinity)
.buttonStyle(RoundedRectButtonStyle(color: Color.red, textColor: Color.white))
}
.frame(minWidth: 200, minHeight: 200)
.alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) {
Button("Yes", role: .destructive) {
unsubscribe(subscription)
}
Button("No", role: .cancel) {
viewModel.subscriptionNameToCancel = nil
}
}
}
}