diff --git a/ClientKit/Sources/ClientKit/Models/StatusModel.swift b/ClientKit/Sources/ClientKit/Models/StatusModel.swift index 068f750..64e16c1 100644 --- a/ClientKit/Sources/ClientKit/Models/StatusModel.swift +++ b/ClientKit/Sources/ClientKit/Models/StatusModel.swift @@ -107,6 +107,18 @@ public extension StatusModel { } } +public extension [StatusModel] { + func getAllImagesUrls() -> [URL] { + var urls: [URL] = [] + + for status in self { + urls.append(contentsOf: status.mediaAttachments.map({ $0.url })) + } + + return urls + } +} + public extension [Status] { func toStatusModels() -> [StatusModel] { self diff --git a/Vernissage/Services/HomeTimelineService.swift b/Vernissage/Services/HomeTimelineService.swift index 734933b..e2514ff 100644 --- a/Vernissage/Services/HomeTimelineService.swift +++ b/Vernissage/Services/HomeTimelineService.swift @@ -9,6 +9,7 @@ import CoreData import PixelfedKit import ClientKit import ServicesKit +import Nuke /// Service responsible for managing home timeline. public class HomeTimelineService { @@ -16,6 +17,7 @@ public class HomeTimelineService { private init() { } private let defaultAmountOfDownloadedStatuses = 40 + private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) public func loadOnBottom(for account: AccountModel) async throws -> Int { // Load data from API and operate on CoreData on background context. @@ -29,26 +31,36 @@ public class HomeTimelineService { } // Load data on bottom of the list. - let newStatuses = try await self.load(for: account, on: backgroundContext, maxId: oldestStatus.id) + let allStatusesFromApi = try await self.load(for: account, on: backgroundContext, maxId: oldestStatus.id) // Save data into database. CoreDataHandler.shared.save(viewContext: backgroundContext) + // Start prefetching images. + self.prefetch(statuses: allStatusesFromApi) + // Return amount of newly downloaded statuses. - return newStatuses.count + return allStatusesFromApi.count } public func refreshTimeline(for account: AccountModel) async throws -> String? { // Load data from API and operate on CoreData on background context. let backgroundContext = CoreDataHandler.shared.newBackgroundContext() + // Retrieve newest visible status (last visible by user). + let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext) + let lastSeenStatusId = dbNewestStatus?.rebloggedStatusId ?? dbNewestStatus?.id + // Refresh/load home timeline (refreshing on top downloads always first 40 items). // When Apple introduce good way to show new items without scroll to top then we can change that method. - let lastSeenStatusId = try await self.refresh(for: account, on: backgroundContext) + let allStatusesFromApi = try await self.refresh(for: account, on: backgroundContext) // Save data into database. CoreDataHandler.shared.save(viewContext: backgroundContext) + // Start prefetching images. + self.prefetch(statuses: allStatusesFromApi) + // Return id of last seen status. return lastSeenStatusId } @@ -134,19 +146,15 @@ public class HomeTimelineService { return amountOfStatuses } - private func refresh(for account: AccountModel, on backgroundContext: NSManagedObjectContext) async throws -> String? { + private func refresh(for account: AccountModel, on backgroundContext: NSManagedObjectContext) async throws -> [Status] { guard let accessToken = account.accessToken else { - return nil + return [] } // Retrieve statuses from API. let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken) let statuses = try await client.getHomeTimeline(limit: self.defaultAmountOfDownloadedStatuses) - // Retrieve newest visible status (last visible by user). - let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext) - let lastSeenStatusId = dbNewestStatus?.rebloggedStatusId ?? dbNewestStatus?.id - // Update all existing statuses in database. for status in statuses { if let dbStatus = StatusDataHandler.shared.getStatusData(accountId: account.id, statusId: status.id, viewContext: backgroundContext) { @@ -196,7 +204,8 @@ public class HomeTimelineService { _ = try await self.add(statusesToAdd, for: account, on: backgroundContext) } - return lastSeenStatusId + // Return all statuses downloaded from API. + return statuses } private func load(for account: AccountModel, @@ -213,13 +222,16 @@ public class HomeTimelineService { let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: self.defaultAmountOfDownloadedStatuses) // Save statuses in database. - return try await self.add(statuses, for: account, on: backgroundContext) + try await self.add(statuses, for: account, on: backgroundContext) + + // Return all statuses downloaded from API. + return statuses } private func add(_ statuses: [Status], for account: AccountModel, on backgroundContext: NSManagedObjectContext - ) async throws -> [Status] { + ) async throws { guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else { throw DatabaseError.cannotDownloadAccount @@ -237,8 +249,6 @@ public class HomeTimelineService { self.copy(from: status, to: statusData, on: backgroundContext) } - - return statusesWithImages } private func copy(from status: Status, @@ -287,4 +297,9 @@ public class HomeTimelineService { } } } + + private func prefetch(statuses: [Status]) { + let statusModels = statuses.getStatusesWithImagesOnly().toStatusModels() + imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls()) + } } diff --git a/Vernissage/Views/PaginableStatusesView.swift b/Vernissage/Views/PaginableStatusesView.swift index c012881..ab6579b 100644 --- a/Vernissage/Views/PaginableStatusesView.swift +++ b/Vernissage/Views/PaginableStatusesView.swift @@ -5,6 +5,7 @@ // import SwiftUI +import Nuke import PixelfedKit import ClientKit import ServicesKit @@ -38,6 +39,7 @@ struct PaginableStatusesView: View { @State private var page = 1 private let defaultLimit = 10 + private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) var body: some View { self.mainBody() @@ -122,6 +124,9 @@ struct PaginableStatusesView: View { inPlaceStatuses.append(StatusModel(status: item)) } + // Prefetch images. + self.prefetch(statusModels: inPlaceStatuses) + self.statusViewModels.append(contentsOf: inPlaceStatuses) } @@ -140,6 +145,9 @@ struct PaginableStatusesView: View { inPlaceStatuses.append(StatusModel(status: item)) } + // Prefetch images. + self.prefetch(statusModels: inPlaceStatuses) + self.statusViewModels.append(contentsOf: inPlaceStatuses) } @@ -152,4 +160,8 @@ struct PaginableStatusesView: View { return try await self.client.accounts?.bookmarks(limit: self.defaultLimit, page: self.page) ?? [] } } + + private func prefetch(statusModels: [StatusModel]) { + imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls()) + } } diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index 2c51985..12093c7 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -5,6 +5,7 @@ // import SwiftUI +import Nuke import PixelfedKit import ClientKit import ServicesKit @@ -50,6 +51,7 @@ struct StatusesView: View { @State private var lastStatusId: String? private let defaultLimit = 20 + private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) var body: some View { self.mainBody() @@ -150,6 +152,9 @@ struct StatusesView: View { inPlaceStatuses.append(StatusModel(status: item)) } + // Prefetch images. + self.prefetch(statusModels: inPlaceStatuses) + // Append to empty list. self.statusViewModels.append(contentsOf: inPlaceStatuses) } @@ -174,6 +179,9 @@ struct StatusesView: View { inPlaceStatuses.append(StatusModel(status: item)) } + // Prefetch images. + self.prefetch(statusModels: inPlaceStatuses) + // Append statuses to existing array of statuses (at the end). self.statusViewModels.append(contentsOf: inPlaceStatuses) } @@ -291,4 +299,8 @@ struct StatusesView: View { ErrorService.shared.handle(error, message: "statuses.error.tagUnfollowFailed", showToastr: true) } } + + private func prefetch(statusModels: [StatusModel]) { + imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls()) + } } diff --git a/Vernissage/Widgets/ImageRowItem.swift b/Vernissage/Widgets/ImageRowItem.swift index be5740b..0b00df2 100644 --- a/Vernissage/Widgets/ImageRowItem.swift +++ b/Vernissage/Widgets/ImageRowItem.swift @@ -22,7 +22,6 @@ struct ImageRowItem: View { @State private var showThumbImage = false @State private var cancelled = true @State private var error: Error? - @State private var opacity = 0.0 @State private var isFavourited = false private let onImageDownloaded: (Double, Double) -> Void @@ -58,21 +57,9 @@ struct ImageRowItem: View { } } } - .opacity(self.opacity) - .onAppear { - withAnimation { - self.opacity = 1.0 - } - } } else { self.imageContainerView(uiImage: uiImage) .imageContextMenu(statusData: self.status, attachmentData: self.attachmentData, uiImage: uiImage) - .opacity(self.opacity) - .onAppear { - withAnimation { - self.opacity = 1.0 - } - } } } else { if cancelled { diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift index 97f0b31..47ee8f8 100644 --- a/Vernissage/Widgets/ImageRowItemAsync.swift +++ b/Vernissage/Widgets/ImageRowItemAsync.swift @@ -22,7 +22,6 @@ struct ImageRowItemAsync: View { private let showAvatar: Bool @State private var showThumbImage = false - @State private var opacity = 0.0 @State private var isFavourited = false private let onImageDownloaded: (Double, Double) -> Void @@ -61,30 +60,20 @@ struct ImageRowItemAsync: View { } } } - .opacity(self.opacity) .onAppear { if let uiImage = state.imageResponse?.image { self.recalculateSizeOfDownloadedImage(uiImage: uiImage) } - - withAnimation { - self.opacity = 1.0 - } } } else { self.imageContainerView(image: image) .imageContextMenu(statusModel: self.statusViewModel, attachmentModel: self.attachment, uiImage: state.imageResponse?.image) - .opacity(self.opacity) .onAppear { if let uiImage = state.imageResponse?.image { self.recalculateSizeOfDownloadedImage(uiImage: uiImage) } - - withAnimation { - self.opacity = 1.0 - } } } } else if state.error != nil {