mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Better handling of delete undo
This commit is contained in:
parent
e9405d9738
commit
a780b24ba1
11 changed files with 71 additions and 145 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ import Views
|
|||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
},
|
||||
label: { Label("Delete", systemImage: "trash") }
|
||||
label: { Label("Remove", systemImage: "trash") }
|
||||
)
|
||||
if FeatureFlag.enableSnooze {
|
||||
Button {
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue