Better handling of delete undo

This commit is contained in:
Jackson Harper 2023-07-14 16:28:06 +08:00
parent e9405d9738
commit a780b24ba1
11 changed files with 71 additions and 145 deletions

View file

@ -84,13 +84,6 @@ public final class Services {
Task {
do {
let fetchedItemCount = try await services.dataService.fetchLinkedItemsBackgroundTask()
EventTracker.track(
.backgroundFetch(
jobStatus: .success,
itemCount: fetchedItemCount,
secondsElapsed: Int(startTime.timeIntervalSinceNow)
)
)
task.setTaskCompleted(success: true)
} catch {
EventTracker.track(

View file

@ -356,8 +356,8 @@ struct AnimatingCellHeight: AnimatableModifier {
}
} else {
Button(
action: { viewModel.undeleteItem(dataService: dataService, itemID: item.unwrappedID) },
label: { Label("Undelete", systemImage: "trash.slash") }
action: { viewModel.recoverItem(dataService: dataService, itemID: item.unwrappedID) },
label: { Label("Recover", systemImage: "trash.slash") }
)
}
}
@ -550,7 +550,7 @@ struct AnimatingCellHeight: AnimatableModifier {
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
},
label: {
Label("Delete", systemImage: "trash")
Label("Remove", systemImage: "trash")
}
).tint(.red))
case .moveToInbox:

View file

@ -74,7 +74,7 @@ import Views
itemToRemove = item
confirmationShown = true
},
label: { Label("Delete", systemImage: "trash") }
label: { Label("Remove", systemImage: "trash") }
)
if FeatureFlag.enableSnooze {
Button {

View file

@ -318,22 +318,13 @@ import Views
}
func removeLink(dataService: DataService, objectID: NSManagedObjectID) {
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: {
self.undeleteItem(dataService: dataService, itemID: itemID)
})
}
removeLibraryItemAction(dataService: dataService, objectID: objectID)
}
func undeleteItem(dataService: DataService, itemID: String) {
func recoverItem(dataService: DataService, itemID: String) {
Task {
if await dataService.undeleteItem(itemID: itemID) {
Snackbar.show(message: "Link undeleted")
if await dataService.recoverItem(itemID: itemID) {
Snackbar.show(message: "Item recovered")
} else {
Snackbar.show(message: "Error. Check trash to recover.")
}

View file

@ -30,8 +30,7 @@ import Views
func handleDeleteAction(dataService: DataService) {
guard let objectID = item?.objectID ?? pdfItem?.objectID else { return }
showInSnackbar("Link removed")
dataService.removeLink(objectID: objectID)
removeLibraryItemAction(dataService: dataService, objectID: objectID)
}
func updateItemReadStatus(dataService: DataService) {
@ -70,43 +69,17 @@ import Views
struct LinkItemDetailView: View {
@EnvironmentObject var authenticator: Authenticator
@EnvironmentObject var dataService: DataService
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
static let navBarHeight = 50.0
let linkedItemObjectID: NSManagedObjectID
let isPDF: Bool
@StateObject private var viewModel = LinkItemDetailViewModel()
@State private var showFontSizePopover = false
@State private var showTitleEdit = false
@State private var navBarVisibilityRatio = 1.0
@State private var showDeleteConfirmation = false
init(linkedItemObjectID: NSManagedObjectID, isPDF: Bool) {
self.linkedItemObjectID = linkedItemObjectID
self.isPDF = isPDF
}
var toggleReadStatusToolbarItem: some View {
Button(
action: {
viewModel.updateItemReadStatus(dataService: dataService)
},
label: {
Image(systemName: viewModel.isItemRead ? "line.horizontal.3.decrease.circle" : "checkmark.circle")
}
)
}
var removeLinkToolbarItem: some View {
Button(
action: { print("delete item action") },
label: {
Image(systemName: "trash")
}
)
}
var body: some View {
ZStack { // Using ZStack so .task can be used on if/else body
if isPDF {
@ -123,76 +96,6 @@ struct LinkItemDetailView: View {
#endif
}
var navBar: some View {
HStack(alignment: .center) {
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
label: {
Image(systemName: "chevron.backward")
.font(.appNavbarIcon)
.foregroundColor(.appGrayTextContrast)
.padding(.horizontal)
}
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showFontSizePopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
Menu(
content: {
Group {
Button(
action: { showTitleEdit = true },
label: { Label("Edit Info", systemImage: "info.circle") }
)
Button(
action: { viewModel.handleArchiveAction(dataService: dataService) },
label: {
Label(
viewModel.isItemArchived ? "Unarchive" : "Archive",
systemImage: viewModel.isItemArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
}
)
Button(
action: { showDeleteConfirmation = true },
label: { Label("Delete", systemImage: "trash") }
)
}
},
label: {
Image(systemName: "ellipsis")
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
}
)
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.onTapGesture {
showFontSizePopover = false
}
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
Button("Remove Link", role: .destructive) {
viewModel.handleDeleteAction(dataService: dataService)
}
Button(LocalText.cancelGeneric, role: .cancel, action: {})
}
.sheet(isPresented: $showTitleEdit) {
if let item = viewModel.item {
LinkedItemMetadataEditView(item: item)
}
}
}
@ViewBuilder private var pdfContainerView: some View {
if let pdfItem = viewModel.pdfItem, let pdfURL = pdfItem.pdfURL {
#if os(iOS)

View file

@ -0,0 +1,40 @@
import CoreData
import Foundation
import Models
import Services
import Views
func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObjectID) {
dataService.viewContext.performAndWait {
if let item = dataService.viewContext.object(with: objectID) as? LinkedItem {
item.state = "DELETED"
try? dataService.viewContext.save()
}
}
let syncTask = Task.detached(priority: .background) {
do {
try await Task.sleep(nanoseconds: 4_000_000_000)
let canceled = Task.isCancelled
if !canceled {
print("syncing link deletion")
dataService.removeLink(objectID: objectID, sync: true)
}
} catch {
print("error running task: ", error)
}
print("checking if task is canceled: ", Task.isCancelled)
}
Snackbar.show(message: "Item removed", undoAction: {
print("canceling task", syncTask)
syncTask.cancel()
dataService.viewContext.performAndWait {
if let item = dataService.viewContext.object(with: objectID) as? LinkedItem {
item.state = "SUCCEEDED"
try? dataService.viewContext.save()
}
}
})
}

View file

@ -19,7 +19,6 @@ struct WebReaderContainerView: View {
@State private var hasPerformedHighlightMutations = false
@State var showHighlightAnnotationModal = false
@State private var navBarVisibilityRatio = 1.0
@State private var showDeleteConfirmation = false
@State private var progressViewOpacity = 0.0
@State var readerSettingsChangedTransactionID: UUID?
@State var annotationSaveTransactionID: UUID?
@ -250,7 +249,7 @@ struct WebReaderContainerView: View {
)
Button(
action: delete,
label: { Label("Delete", systemImage: "trash") }
label: { Label("Remove", systemImage: "trash") }
)
Button(
action: {
@ -346,17 +345,6 @@ struct WebReaderContainerView: View {
.opacity(navBarVisibilityRatio)
.foregroundColor(ThemeManager.currentTheme.isDark ? .white : .black)
.background(ThemeManager.currentBgColor)
.alert("Are you sure you want to remove this item? All associated notes and highlights will be deleted.",
isPresented: $showDeleteConfirmation) {
Button("Remove Item", role: .destructive) {
Snackbar.show(message: "Link removed")
dataService.removeLink(objectID: item.objectID)
#if os(iOS)
presentationMode.wrappedValue.dismiss()
#endif
}
Button(LocalText.cancelGeneric, role: .cancel, action: {})
}
.sheet(isPresented: $showLabelsModal) {
ApplyLabelsView(mode: .item(item), isSearchFocused: false, onSave: { labels in
showLabelsModal = false
@ -607,7 +595,12 @@ struct WebReaderContainerView: View {
}
func delete() {
showDeleteConfirmation = true
removeLibraryItemAction(dataService: dataService, objectID: item.objectID)
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
presentationMode.wrappedValue.dismiss()
}
#endif
}
func editLabels() {

View file

@ -38,7 +38,9 @@ public extension LinkedItemFilter {
var predicate: NSPredicate {
let undeletedPredicate = NSPredicate(
format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue)
format: "%K != %i AND %K != \"DELETED\"",
#keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue),
#keyPath(LinkedItem.state)
)
let notInArchivePredicate = NSPredicate(
format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber

View file

@ -4,7 +4,7 @@ import Models
import SwiftGraphQL
public extension DataService {
func removeLink(objectID: NSManagedObjectID) {
func removeLink(objectID: NSManagedObjectID, sync: Bool = true) {
// First try to get the item synchronously, this is used later to delete files
// Then we can async update core data and make the API call to sync the deletion
@ -28,8 +28,9 @@ public extension DataService {
logger.debug("Failed to mark LinkedItem for deletion: \(error.localizedDescription)")
}
// Send update to server
self.syncLinkDeletion(itemID: linkedItem.unwrappedID)
if sync {
self.syncLinkDeletion(itemID: linkedItem.unwrappedID)
}
}
if let linkedItemID = linkedItemID {

View file

@ -4,7 +4,7 @@ import Models
import SwiftGraphQL
public extension DataService {
func undeleteItem(itemID: String) async -> Bool {
func recoverItem(itemID: String) async -> Bool {
var itemUpdatedLocal = false
// If the item is still available locally, update its state
backgroundContext.performAndWait {
@ -22,12 +22,12 @@ public extension DataService {
}
}
// If we undeleted locally, but failed to sync the undelete, that is OK, because
// If we recovered 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
return await syncServerRecoverItem(itemID: itemID) || itemUpdatedLocal
}
func syncServerUndeleteItem(itemID: String) async -> Bool {
func syncServerRecoverItem(itemID: String) async -> Bool {
enum MutationResult {
case saved(title: String)
case error(errorMessage: String)

View file

@ -42,7 +42,10 @@ public struct Snackbar: View {
.foregroundColor(self.colorScheme == .light ? .white : .appTextDefault)
Spacer()
if let undoAction = operation.undoAction {
Button("Undo", action: undoAction)
Button("Undo", action: {
isShowing = false
undoAction()
})
.font(.system(size: 16, weight: .bold))
}
}