diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 837b41376..330c2bcca 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -6,6 +6,8 @@ import Utils import Views @MainActor final class HomeFeedViewModel: NSObject, ObservableObject { + let dateFormatter = DateFormatter.formatterISO8601 + var currentDetailViewModel: LinkItemDetailViewModel? private var fetchedResultsController: NSFetchedResultsController? @@ -26,7 +28,11 @@ import Views @Published var appliedFilter = LinkedItemFilter.inbox.rawValue @Published var appliedSort = LinkedItemSort.newest.rawValue - @AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) +// @AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) // TODO: missing value here? + // TODO: delete this value when needed (logout, clear core data, new app version + @AppStorage(UserDefaultKey.lastItemSyncTime.rawValue) var lastItemSyncTime = Date( + timeIntervalSinceReferenceDate: 0 + ).toString() var cursor: String? @@ -50,6 +56,11 @@ import Views items.insert(item, at: 0) } + func loadUpdatedItems(since _: Date, updatedCursor _: String?) { + isLoading = true + showLoadingBar = true + } + func loadItems(dataService: DataService, isRefresh: Bool) async { let thisSearchIdx = searchIdx searchIdx += 1 @@ -62,6 +73,14 @@ import Views Task { _ = try? await dataService.fetchViewer() } } + // Sync items if necessary + let lastSyncDate = dateFormatter.date(from: lastItemSyncTime) ?? Date(timeIntervalSinceReferenceDate: 0) + + let syncResult = try? await dataService.syncLinkedItems(since: lastSyncDate, cursor: nil) + if syncResult != nil { + lastItemSyncTime = Date().toString() + } + let queryResult = try? await dataService.loadLinkedItems( limit: 10, searchQuery: searchQuery, diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index bcdff4116..0d9aadb5f 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -154,4 +154,12 @@ public extension LinkedItem { } } } + + static func deleteItems(ids: [String], context: NSManagedObjectContext) { + for itemID in ids { + if let linkedItem = lookup(byID: itemID, inContext: context) { + linkedItem.remove(inContext: context) + } + } + } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift index 24c6a5738..66dc75968 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -3,6 +3,49 @@ import Foundation import Models public extension DataService { + /// Requests `LinkedItem`s updates from the server since a certain datae + /// and stores it in CoreData while deleting all the items with ids the server says + /// have been deleted. + /// - Parameters: + /// - limit: max count of items + /// - searchQuery: search terms and filters + /// - cursor: cursor when loading batch for infinite list + /// - Returns: `LinkedItemQueryResult` (managed object IDs and an optional cursor) + func syncLinkedItems( + since date: Date, + cursor: String?, + previousQueryResult: LinkedItemQueryResult? = nil + ) async throws -> LinkedItemQueryResult { + if previousQueryResult == nil { + // Send offline changes to server before fetching items + // only on the first call of this function + try? await syncOfflineItemsWithServerIfNeeded() + } + + let fetchResult = try await linkedItemUpdates(since: date, limit: 20, cursor: cursor) + + LinkedItem.deleteItems(ids: fetchResult.deletedItemIDs, context: backgroundContext) + + guard let itemIDs = fetchResult.items.persist(context: backgroundContext) else { + throw BasicError.message(messageText: "CoreData error") + } + + let result = LinkedItemQueryResult( + itemIDs: itemIDs + (previousQueryResult?.itemIDs ?? []), + cursor: fetchResult.cursor + ) + + if fetchResult.hasMoreItems, (previousQueryResult?.itemIDs.count ?? 0) < 200 { + return try await syncLinkedItems( + since: date, + cursor: fetchResult.cursor, + previousQueryResult: result + ) + } + + return result + } + /// Requests `LinkedItem`s from the server and stores it in CoreData. /// - Parameters: /// - limit: max count of items diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index 52f8a72a0..348e26cd9 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -12,6 +12,7 @@ struct InternalLinkedItemUpdatesQueryResult { let items: [InternalLinkedItem] let deletedItemIDs: [String] let cursor: String? + let hasMoreItems: Bool } private struct SyncItemEdge { @@ -23,13 +24,14 @@ private struct SyncItemEdge { extension DataService { // swiftlint:disable:next function_body_length func linkedItemUpdates( + since: Date, limit: Int, - cursor: String?, - since: Date? + cursor: String? ) async throws -> InternalLinkedItemUpdatesQueryResult { struct QuerySuccessResult { let edges: [SyncItemEdge] let cursor: String? + let hasMoreItems: Bool } enum QueryResult { case success(result: QuerySuccessResult) @@ -50,6 +52,9 @@ extension DataService { edges: try $0.edges(selection: syncItemEdgeSelection.list), cursor: try $0.pageInfo(selection: Selection.PageInfo { try $0.endCursor() + }), + hasMoreItems: try $0.pageInfo(selection: Selection.PageInfo { + try $0.hasNextPage() }) ) ) @@ -61,7 +66,7 @@ extension DataService { try $0.updatesSince( after: OptionalArgument(cursor), first: OptionalArgument(limit), - since: DateTime(from: since ?? Date(timeIntervalSinceReferenceDate: 0)), + since: DateTime(from: since), selection: selection ) } @@ -90,7 +95,8 @@ extension DataService { returning: InternalLinkedItemUpdatesQueryResult( items: items, deletedItemIDs: deletedItemIDs, - cursor: result.cursor + cursor: result.cursor, + hasMoreItems: result.hasMoreItems ) ) case let .error(error): diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index 29ed93b7b..deab520db 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -12,4 +12,5 @@ public enum UserDefaultKey: String { case lastSelectedLinkedItemFilter case lastUsedAppVersion case lastUsedAppBuildNumber + case lastItemSyncTime }