Add undelete
This commit is contained in:
@ -3,7 +3,7 @@ import Services
|
||||
import Views
|
||||
|
||||
extension Snackbar {
|
||||
static func show(message: String) {
|
||||
NSNotification.operationSuccess(message: message)
|
||||
static func show(message: String, undoAction: (() -> Void)? = nil) {
|
||||
NSNotification.operationSuccess(message: message, undoAction: undoAction)
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,9 +257,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
|
||||
@Binding var prefersListLayout: Bool
|
||||
|
||||
@State private var itemToRemove: LinkedItem?
|
||||
@State private var confirmationShown = false
|
||||
@State private var showHideFeatureAlert = false
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
@ -344,8 +341,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
)
|
||||
})
|
||||
Button("Remove Item", role: .destructive) {
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
|
||||
}
|
||||
if let author = item.author {
|
||||
Button(
|
||||
@ -505,18 +501,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
.padding(0)
|
||||
.listStyle(PlainListStyle())
|
||||
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.alert("Are you sure you want to delete this item? All associated notes and highlights will be deleted.",
|
||||
isPresented: $confirmationShown) {
|
||||
Button("Remove Item", role: .destructive) {
|
||||
if let itemToRemove = itemToRemove {
|
||||
withAnimation {
|
||||
viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID)
|
||||
}
|
||||
}
|
||||
self.itemToRemove = nil
|
||||
}
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.itemToRemove = nil }
|
||||
}
|
||||
}
|
||||
.alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.",
|
||||
isPresented: $showHideFeatureAlert) {
|
||||
@ -556,8 +540,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
case .delete:
|
||||
return AnyView(Button(
|
||||
action: {
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
|
||||
},
|
||||
label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
@ -581,8 +564,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
|
||||
@State private var itemToRemove: LinkedItem?
|
||||
@State private var confirmationShown = false
|
||||
@State var isContextMenuOpen = false
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
@ -594,8 +575,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
case .toggleArchiveStatus:
|
||||
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: !item.isArchived)
|
||||
case .delete:
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
|
||||
case .editLabels:
|
||||
viewModel.itemUnderLabelEdit = item
|
||||
case .editTitle:
|
||||
@ -710,18 +690,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:disable:next line_length
|
||||
.alert("Are you sure you want to delete this item? All associated notes and highlights will be deleted.", isPresented: $confirmationShown) {
|
||||
Button("Delete Item", role: .destructive) {
|
||||
if let itemToRemove = itemToRemove {
|
||||
withAnimation {
|
||||
viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID)
|
||||
}
|
||||
}
|
||||
self.itemToRemove = nil
|
||||
}
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.itemToRemove = nil }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -318,8 +318,22 @@ import Views
|
||||
}
|
||||
|
||||
func removeLink(dataService: DataService, objectID: NSManagedObjectID) {
|
||||
Snackbar.show(message: "Link removed")
|
||||
dataService.removeLink(objectID: objectID)
|
||||
let item = dataService.viewContext.object(with: objectID) as? LinkedItem
|
||||
|
||||
if let item = item {
|
||||
let itemID = item.unwrappedID
|
||||
dataService.removeLink(objectID: objectID)
|
||||
|
||||
Snackbar.show(message: "Link deleted", undoAction: {
|
||||
Task {
|
||||
if await dataService.undeleteItem(itemID: itemID) {
|
||||
Snackbar.show(message: "Link undeleted")
|
||||
} else {
|
||||
Snackbar.show(message: "Error. Check trash to recover.")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getOrCreateLabel(dataService: DataService, named: String, color: String) -> LinkedItemLabel? {
|
||||
|
||||
@ -62,7 +62,7 @@ struct InnerRootView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.snackBar(isShowing: $viewModel.showSnackbar, message: viewModel.snackbarMessage)
|
||||
.snackBar(isShowing: $viewModel.showSnackbar, message: viewModel.snackbarMessage, undoAction: viewModel.snackBarUndoAction)
|
||||
// Schedule the dismissal every time we present the snackbar.
|
||||
.onChange(of: viewModel.showSnackbar) { newValue in
|
||||
if newValue {
|
||||
@ -93,8 +93,9 @@ struct InnerRootView: View {
|
||||
#if os(iOS)
|
||||
.onReceive(NSNotification.operationSuccessPublisher) { notification in
|
||||
if let message = notification.userInfo?["message"] as? String {
|
||||
viewModel.showSnackbar = true
|
||||
viewModel.snackbarMessage = message
|
||||
viewModel.snackBarUndoAction = notification.userInfo?["undoAction"] as? (() -> Void)
|
||||
viewModel.showSnackbar = true
|
||||
}
|
||||
}
|
||||
.onReceive(NSNotification.operationFailedPublisher) { notification in
|
||||
|
||||
@ -19,6 +19,7 @@ public final class RootViewModel: ObservableObject {
|
||||
|
||||
@Published var snackbarMessage: String?
|
||||
@Published var showSnackbar = false
|
||||
@Published var snackBarUndoAction: (() -> Void)?
|
||||
@Published var showMiniPlayer = true
|
||||
|
||||
public init() {
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func undeleteItem(itemID: String) async -> Bool {
|
||||
var itemUpdatedLocal = false
|
||||
// If the item is still available locally, update its state
|
||||
backgroundContext.performAndWait {
|
||||
if let linkedItem = LinkedItem.lookup(byID: itemID, inContext: backgroundContext) {
|
||||
linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsUpdate.rawValue)
|
||||
|
||||
do {
|
||||
try backgroundContext.save()
|
||||
itemUpdatedLocal = true
|
||||
logger.debug("LinkedItem updated succesfully")
|
||||
} catch {
|
||||
backgroundContext.rollback()
|
||||
logger.debug("Failed to update LinkedItem: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we undeleted locally, but failed to sync the undelete, that is OK, because
|
||||
// the item shouldn't be deleted server side.
|
||||
return await syncServerUndeleteItem(itemID: itemID) || itemUpdatedLocal
|
||||
}
|
||||
|
||||
func syncServerUndeleteItem(itemID: String) async -> Bool {
|
||||
enum MutationResult {
|
||||
case saved(title: String)
|
||||
case error(errorMessage: String)
|
||||
}
|
||||
|
||||
let selection = Selection<MutationResult, Unions.UpdatePageResult> {
|
||||
try $0.on(
|
||||
updatePageError: .init { .error(errorMessage: try $0.errorCodes().first.toString()) },
|
||||
updatePageSuccess: .init {
|
||||
.saved(title: try $0.updatedPage(selection: Selection.Article { try $0.title() }))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.updatePage(
|
||||
input: .init(pageId: itemID,
|
||||
state: OptionalArgument(Enums.ArticleSavingRequestStatus.succeeded)),
|
||||
selection: selection
|
||||
)
|
||||
}
|
||||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
try? await withCheckedThrowingContinuation { continuation in
|
||||
send(mutation, to: path, headers: headers) { _ in
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
|
||||
let result = try? await loadLinkedItem(username: "me", itemID: itemID)
|
||||
return result != nil
|
||||
}
|
||||
}
|
||||
@ -67,8 +67,10 @@ public extension NSNotification {
|
||||
)
|
||||
}
|
||||
|
||||
static func operationSuccess(message: String) {
|
||||
NotificationCenter.default.post(name: NSNotification.OperationSuccess, object: nil, userInfo: ["message": message])
|
||||
static func operationSuccess(message: String, undoAction: (() -> Void)?) {
|
||||
NotificationCenter.default.post(name: NSNotification.OperationSuccess,
|
||||
object: nil,
|
||||
userInfo: ["message": message, "undoAction": undoAction as Any])
|
||||
}
|
||||
|
||||
static func operationFailed(message: String) {
|
||||
|
||||
@ -5,16 +5,20 @@ public struct Snackbar: View {
|
||||
private let presentingView: AnyView
|
||||
private let text: Text
|
||||
|
||||
private let undoAction: (() -> Void)?
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
|
||||
init<PresentingView>(
|
||||
isShowing: Binding<Bool>,
|
||||
presentingView: PresentingView,
|
||||
text: Text
|
||||
text: Text,
|
||||
undoAction: (() -> Void)?
|
||||
) where PresentingView: View {
|
||||
self._isShowing = isShowing
|
||||
self.presentingView = AnyView(presentingView)
|
||||
self.text = text
|
||||
self.undoAction = undoAction
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
@ -29,6 +33,10 @@ public struct Snackbar: View {
|
||||
.font(.appCallout)
|
||||
.foregroundColor(self.colorScheme == .light ? .white : .appTextDefault)
|
||||
Spacer()
|
||||
if let undoAction = undoAction {
|
||||
Button("Undo", action: undoAction)
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(width: min(380, geometry.size.width * 0.96), height: 44)
|
||||
@ -45,7 +53,7 @@ public struct Snackbar: View {
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func snackBar(isShowing: Binding<Bool>, message: String?) -> some View {
|
||||
Snackbar(isShowing: isShowing, presentingView: self, text: Text(message ?? ""))
|
||||
func snackBar(isShowing: Binding<Bool>, message: String?, undoAction: (() -> Void)?) -> some View {
|
||||
Snackbar(isShowing: isShowing, presentingView: self, text: Text(message ?? ""), undoAction: undoAction)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user