diff --git a/apple/OmnivoreKit/Sources/App/Services.swift b/apple/OmnivoreKit/Sources/App/Services.swift index f438131c4..9015e4f20 100644 --- a/apple/OmnivoreKit/Sources/App/Services.swift +++ b/apple/OmnivoreKit/Sources/App/Services.swift @@ -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( diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 794d27f20..24eb9c03b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -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: diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index 92e269f4a..55cd8b6f9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -74,7 +74,7 @@ import Views itemToRemove = item confirmationShown = true }, - label: { Label("Delete", systemImage: "trash") } + label: { Label("Remove", systemImage: "trash") } ) if FeatureFlag.enableSnooze { Button { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 613900d6c..9e81ab861 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -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.") } diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 4237d1e0c..a36363df5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -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 - 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) diff --git a/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift b/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift new file mode 100644 index 000000000..cd7441c4e --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift @@ -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() + } + } + }) +} diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 6583c669c..78dcfd504 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -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() { diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift index 2348d0720..eb9975b47 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -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 diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift index 778940295..36e7aa173 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift @@ -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 { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift index 74dfa797c..50920d45f 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/Views/SnackBar.swift b/apple/OmnivoreKit/Sources/Views/SnackBar.swift index 4e5cf9272..e7abe4b19 100644 --- a/apple/OmnivoreKit/Sources/Views/SnackBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SnackBar.swift @@ -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)) } }