diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 6b3b0c4..1c552db 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; }; F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; }; F8341F90295C636C009C8EE6 /* Data+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* Data+Exif.swift */; }; - F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; }; F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; }; F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; }; F85D4973296406E700751DF7 /* BottomRight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4972296406E700751DF7 /* BottomRight.swift */; }; @@ -105,7 +104,6 @@ F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = ""; }; F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = ""; }; F8341F8F295C636C009C8EE6 /* Data+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Exif.swift"; sourceTree = ""; }; - F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = ""; }; F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = ""; }; F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = ""; }; F85D4972296406E700751DF7 /* BottomRight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRight.swift; sourceTree = ""; }; @@ -235,7 +233,6 @@ isa = PBXGroup; children = ( F8C14399296B2150001FE31D /* Errors */, - F8341F91295C63BB009C8EE6 /* ImageStatus.swift */, F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */, F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */, F8C14397296B208A001FE31D /* HTTPStatusCode.swift */, @@ -493,7 +490,6 @@ F85DBF8F296732E20069BF89 /* FollowersView.swift in Sources */, F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */, F85D49872964334100751DF7 /* String+Date.swift in Sources */, - F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, F897978829681B9C00B22335 /* UserAvatar.swift in Sources */, F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */, F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */, diff --git a/Vernissage/Models/ImageStatus.swift b/Vernissage/Models/ImageStatus.swift deleted file mode 100644 index ba29463..0000000 --- a/Vernissage/Models/ImageStatus.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// https://mczachurski.dev -// Copyright © 2022 Marcin Czachurski and the repository contributors. -// Licensed under the MIT License. -// - -import Foundation -import UIKit -import MastodonSwift - -public struct ImageStatus: Identifiable { - public let id: String - public let image: UIImage - public let status: Status -} diff --git a/Vernissage/Services/TimelineService.swift b/Vernissage/Services/TimelineService.swift index 1b8479d..a29ec4d 100644 --- a/Vernissage/Services/TimelineService.swift +++ b/Vernissage/Services/TimelineService.swift @@ -57,12 +57,26 @@ public class TimelineService { // Retrieve statuses from API. let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken) - let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 40) + let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 20) + // Download all images from server. + let attachmentsData = await self.fetchAllImages(statuses: statuses) + // Save status data in database. for status in statuses { + let contains = attachmentsData.contains { (key: String, value: Data) in + status.mediaAttachments.contains { attachment in + attachment.id == key + } + } + + // We are adding status only when we have at least one image for status. + if contains == false { + continue + } + let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext) - try await self.copy(from: status, to: statusData, on: backgroundContext) + try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext) } try backgroundContext.save() @@ -72,20 +86,21 @@ public class TimelineService { // Load data from API and operate on CoreData on background context. let backgroundContext = CoreDataHandler.shared.newBackgroundContext() + // Download all images from server. + let attachmentsData = await self.fetchAllImages(statuses: [status]) + // Update status data in database. - try await self.copy(from: status, to: statusData, on: backgroundContext) + try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext) try backgroundContext.save() return statusData } - private func copy(from status: Status, to statusData: StatusData, on backgroundContext: NSManagedObjectContext) async throws { + private func copy(from status: Status, to statusData: StatusData, attachmentsData: Dictionary, on backgroundContext: NSManagedObjectContext) async throws { statusData.copyFrom(status) for attachment in status.mediaAttachments { - let imageData = try await self.fetchImage(attachment: attachment) - - guard let imageData = imageData else { + guard let imageData = attachmentsData[attachment.id] else { continue } @@ -127,8 +142,46 @@ public class TimelineService { } } - private func fetchImage(attachment: Attachment) async throws -> Data? { - guard let data = try await RemoteFileService.shared.fetchData(url: attachment.url) else { + private func fetchAllImages(statuses: [Status]) async -> Dictionary { + var attachmentUrls: Dictionary = [:] + + statuses.forEach { status in + status.mediaAttachments.forEach { attachment in + attachmentUrls[attachment.id] = attachment.url + } + } + + return await withTaskGroup(of: (String, Data?).self, returning: [String : Data].self) { taskGroup in + for attachmentUrl in attachmentUrls { + taskGroup.addTask { + do { + if let imageData = try await self.fetchImage(attachmentUrl: attachmentUrl.value) { + return (attachmentUrl.key, imageData) + } + + return (attachmentUrl.key, nil) + } catch { + print("Error \(error.localizedDescription)") + return (attachmentUrl.key, nil) + } + } + } + + var childTaskResults = [String: Data]() + for await result in taskGroup { + guard let data = result.1 else { + continue + } + + childTaskResults[result.0] = data + } + + return childTaskResults + } + } + + private func fetchImage(attachmentUrl: URL) async throws -> Data? { + guard let data = try await RemoteFileService.shared.fetchData(url: attachmentUrl) else { return nil } diff --git a/Vernissage/Widgets/ImageRow.swift b/Vernissage/Widgets/ImageRow.swift index 4c05bc7..559fe30 100644 --- a/Vernissage/Widgets/ImageRow.swift +++ b/Vernissage/Widgets/ImageRow.swift @@ -8,8 +8,10 @@ import SwiftUI struct ImageRow: View { @State public var status: StatusData - @State private var showSensitive = false - + + @State private var imageHeight = UIScreen.main.bounds.width + @State private var imageWidth = UIScreen.main.bounds.width + var body: some View { if let attachmenData = self.status.attachments().first, let uiImage = UIImage(data: attachmenData.data) { @@ -39,8 +41,24 @@ struct ImageRow: View { }.padding() } } + .frame(width: self.imageWidth, height: self.imageHeight) + .onAppear { + self.recalculateSizeOfDownloadedImage(uiImage: uiImage) + } } } + + private func recalculateSizeOfDownloadedImage(uiImage: UIImage) { + let imgHeight = uiImage.size.height + let imgWidth = uiImage.size.width + let calculatedHeight = self.calculateHeight(width: imgWidth, height: imgHeight) + self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width + } + + private func calculateHeight(width: Double, height: Double) -> CGFloat { + let divider = width / UIScreen.main.bounds.size.width + return height / divider + } } struct ImageRow_Previews: PreviewProvider { diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift index c948238..c4c9b87 100644 --- a/Vernissage/Widgets/ImageRowAsync.swift +++ b/Vernissage/Widgets/ImageRowAsync.swift @@ -10,10 +10,10 @@ import NukeUI struct ImageRowAsync: View { @State public var status: Status + @State private var imageHeight = UIScreen.main.bounds.width @State private var imageWidth = UIScreen.main.bounds.width @State private var heightWasPrecalculated = true - @State private var showSensitive = false var body: some View { if let attachment = status.mediaAttachments.first {