From 0d493a20a33c7424b1b0f69f8b3d28decaecf05a Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Sun, 1 Jan 2023 18:13:36 +0100 Subject: [PATCH] Store statuses in database --- Vernissage.xcodeproj/project.pbxproj | 24 +++ Vernissage/CoreData/AccountDataHandler.swift | 17 +++ .../AttachmentData+CoreDataClass.swift | 15 ++ .../AttachmentData+CoreDataProperties.swift | 34 +++++ .../CoreData/AttachmentDataHandler.swift | 26 ++++ .../CoreData/StatusData+CoreDataClass.swift | 15 ++ .../StatusData+CoreDataProperties.swift | 65 +++++++++ Vernissage/CoreData/StatusDataHandler.swift | 41 ++++++ .../Vernissage.xcdatamodel/contents | 38 +++++ Vernissage/VernissageApp.swift | 10 +- Vernissage/Views/DetailsView.swift | 28 ++-- Vernissage/Views/HomeFeedView.swift | 125 +++++++++++----- Vernissage/Views/SignInView.swift | 138 ++++++++++-------- 13 files changed, 459 insertions(+), 117 deletions(-) create mode 100644 Vernissage/CoreData/AttachmentData+CoreDataClass.swift create mode 100644 Vernissage/CoreData/AttachmentData+CoreDataProperties.swift create mode 100644 Vernissage/CoreData/AttachmentDataHandler.swift create mode 100644 Vernissage/CoreData/StatusData+CoreDataClass.swift create mode 100644 Vernissage/CoreData/StatusData+CoreDataProperties.swift create mode 100644 Vernissage/CoreData/StatusDataHandler.swift diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index b7a4c98..2bad2c1 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */; }; + F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */; }; + F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048012961850500E6868A /* StatusData+CoreDataClass.swift */; }; + F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; }; + F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; }; + F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; }; F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage.swift */; }; F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; }; F83901A4295D864D00456AE2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A3295D864D00456AE2 /* TagView.swift */; }; @@ -38,6 +44,12 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+CoreDataClass.swift"; sourceTree = ""; }; + F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+CoreDataProperties.swift"; sourceTree = ""; }; + F80048012961850500E6868A /* StatusData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+CoreDataClass.swift"; sourceTree = ""; }; + F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+CoreDataProperties.swift"; sourceTree = ""; }; + F80048072961E6DE00E6868A /* StatusDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDataHandler.swift; sourceTree = ""; }; + F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = ""; }; F8341F8F295C636C009C8EE6 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = ""; }; F83901A3295D864D00456AE2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; @@ -116,6 +128,10 @@ F8341F96295C6427009C8EE6 /* CoreData */ = { isa = PBXGroup; children = ( + F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */, + F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */, + F80048012961850500E6868A /* StatusData+CoreDataClass.swift */, + F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */, F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */, F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */, F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */, @@ -123,6 +139,8 @@ F88C2474295C37BB0006098B /* CoreDataHandler.swift */, F866F6A229604161002E8F88 /* AccountDataHandler.swift */, F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */, + F80048072961E6DE00E6868A /* StatusDataHandler.swift */, + F80048092961EA1900E6868A /* AttachmentDataHandler.swift */, ); path = CoreData; sourceTree = ""; @@ -272,14 +290,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */, F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */, F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */, F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */, + F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */, F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */, F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */, F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */, F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, + F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */, + F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */, F83901A6295D8EC000456AE2 /* LabelIconView.swift in Sources */, + F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */, + F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */, F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */, F88C246E295C37B80006098B /* MainView.swift in Sources */, F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */, diff --git a/Vernissage/CoreData/AccountDataHandler.swift b/Vernissage/CoreData/AccountDataHandler.swift index 34fe0d4..12e7fb2 100644 --- a/Vernissage/CoreData/AccountDataHandler.swift +++ b/Vernissage/CoreData/AccountDataHandler.swift @@ -18,6 +18,23 @@ class AccountDataHandler { return [] } } + + func getCurrentAccountData() -> AccountData? { + let accounts = self.getAccountsData() + + let applicationSettingsHandler = ApplicationSettingsHandler() + let defaultSettings = applicationSettingsHandler.getDefaultSettings() + + let currentAccount = accounts.first { accountData in + accountData.id == defaultSettings.currentAccount + } + + if let currentAccount { + return currentAccount + } + + return accounts.first + } func createAccountDataEntity() -> AccountData { let context = CoreDataHandler.shared.container.viewContext diff --git a/Vernissage/CoreData/AttachmentData+CoreDataClass.swift b/Vernissage/CoreData/AttachmentData+CoreDataClass.swift new file mode 100644 index 0000000..a3f6674 --- /dev/null +++ b/Vernissage/CoreData/AttachmentData+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +// + +import Foundation +import CoreData + +@objc(AttachmentData) +public class AttachmentData: NSManagedObject { + +} diff --git a/Vernissage/CoreData/AttachmentData+CoreDataProperties.swift b/Vernissage/CoreData/AttachmentData+CoreDataProperties.swift new file mode 100644 index 0000000..69382df --- /dev/null +++ b/Vernissage/CoreData/AttachmentData+CoreDataProperties.swift @@ -0,0 +1,34 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +// + +import Foundation +import CoreData + + +extension AttachmentData { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "AttachmentData") + } + + @NSManaged public var blurhash: String? + @NSManaged public var data: Data + @NSManaged public var id: String + @NSManaged public var previewUrl: URL? + @NSManaged public var remoteUrl: URL? + @NSManaged public var statusId: String + @NSManaged public var text: String? + @NSManaged public var type: String + @NSManaged public var url: URL + @NSManaged public var statusRelation: StatusData? + +} + +extension AttachmentData : Identifiable { + +} diff --git a/Vernissage/CoreData/AttachmentDataHandler.swift b/Vernissage/CoreData/AttachmentDataHandler.swift new file mode 100644 index 0000000..8832d68 --- /dev/null +++ b/Vernissage/CoreData/AttachmentDataHandler.swift @@ -0,0 +1,26 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation + +class AttachmentDataHandler { + func getAttachmentsData() -> [AttachmentData] { + let context = CoreDataHandler.shared.container.viewContext + let fetchRequest = AttachmentData.fetchRequest() + do { + return try context.fetch(fetchRequest) + } catch { + print("Error during fetching accounts") + return [] + } + } + + func createAttachmnentDataEntity() -> AttachmentData { + let context = CoreDataHandler.shared.container.viewContext + return AttachmentData(context: context) + } +} diff --git a/Vernissage/CoreData/StatusData+CoreDataClass.swift b/Vernissage/CoreData/StatusData+CoreDataClass.swift new file mode 100644 index 0000000..e1835cb --- /dev/null +++ b/Vernissage/CoreData/StatusData+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +// + +import Foundation +import CoreData + +@objc(StatusData) +public class StatusData: NSManagedObject { + +} diff --git a/Vernissage/CoreData/StatusData+CoreDataProperties.swift b/Vernissage/CoreData/StatusData+CoreDataProperties.swift new file mode 100644 index 0000000..6130b71 --- /dev/null +++ b/Vernissage/CoreData/StatusData+CoreDataProperties.swift @@ -0,0 +1,65 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +// + +import Foundation +import CoreData + + +extension StatusData { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "StatusData") + } + + @NSManaged public var accountAvatar: URL? + @NSManaged public var accountDisplayName: String? + @NSManaged public var accountId: String + @NSManaged public var accountUsername: String + @NSManaged public var applicationName: String? + @NSManaged public var applicationWebsite: URL? + @NSManaged public var bookmarked: Bool + @NSManaged public var content: String + @NSManaged public var createdAt: String + @NSManaged public var favourited: Bool + @NSManaged public var favouritesCount: Int32 + @NSManaged public var id: String + @NSManaged public var inReplyToAccount: String? + @NSManaged public var inReplyToId: String? + @NSManaged public var muted: Bool + @NSManaged public var pinned: Bool + @NSManaged public var reblogged: Bool + @NSManaged public var reblogsCount: Int32 + @NSManaged public var sensitive: Bool + @NSManaged public var spoilerText: String? + @NSManaged public var uri: String? + @NSManaged public var url: URL? + @NSManaged public var visibility: String + @NSManaged public var attachmentRelation: NSSet? + +} + +// MARK: Generated accessors for attachmentRelation +extension StatusData { + + @objc(addAttachmentRelationObject:) + @NSManaged public func addToAttachmentRelation(_ value: AttachmentData) + + @objc(removeAttachmentRelationObject:) + @NSManaged public func removeFromAttachmentRelation(_ value: AttachmentData) + + @objc(addAttachmentRelation:) + @NSManaged public func addToAttachmentRelation(_ values: NSSet) + + @objc(removeAttachmentRelation:) + @NSManaged public func removeFromAttachmentRelation(_ values: NSSet) + +} + +extension StatusData : Identifiable { + +} diff --git a/Vernissage/CoreData/StatusDataHandler.swift b/Vernissage/CoreData/StatusDataHandler.swift new file mode 100644 index 0000000..3c7b1c8 --- /dev/null +++ b/Vernissage/CoreData/StatusDataHandler.swift @@ -0,0 +1,41 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation + +class StatusDataHandler { + func getStatusesData() -> [StatusData] { + let context = CoreDataHandler.shared.container.viewContext + let fetchRequest = StatusData.fetchRequest() + do { + return try context.fetch(fetchRequest) + } catch { + print("Error during fetching accounts") + return [] + } + } + + func getMaximumStatus() -> StatusData? { + let context = CoreDataHandler.shared.container.viewContext + let fetchRequest = StatusData.fetchRequest() + + fetchRequest.fetchLimit = 1 + let sortDescriptor = NSSortDescriptor(key: "id", ascending: false) + fetchRequest.sortDescriptors = [sortDescriptor] + do { + let statuses = try context.fetch(fetchRequest) + return statuses.first + } catch { + return nil + } + } + + func createStatusDataEntity() -> StatusData { + let context = CoreDataHandler.shared.container.viewContext + return StatusData(context: context) + } +} diff --git a/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents b/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents index 71dd21f..1a9569d 100644 --- a/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents +++ b/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents @@ -24,4 +24,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index 0d827b8..e8be64f 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -36,16 +36,17 @@ struct VernissageApp: SwiftUI.App { } .task { let accountDataHandler = AccountDataHandler() - let accounts = accountDataHandler.getAccountsData() + let currentAccount = accountDataHandler.getCurrentAccountData() // When we dont have even one account stored in database then we have to ask user to enter server and sign in. - guard let accountData = accounts.first, let accessToken = accountData.accessToken else { + guard let accountData = currentAccount, let accessToken = accountData.accessToken else { self.applicationViewMode = .signIn return } // When we have at least one account then we have to verify access token. let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken) + do { let account = try await client.verifyCredentials() try await self.updateAccount(accountData: accountData, account: account) @@ -117,6 +118,11 @@ struct VernissageApp: SwiftUI.App { } } + // We have to be sure that account id is saved as default account. + let applicationSettingsHandler = ApplicationSettingsHandler() + let defaultSettings = applicationSettingsHandler.getDefaultSettings() + defaultSettings.currentAccount = accountData.id + // Save account data in database and in application state. try self.coreDataHandler.container.viewContext.save() } diff --git a/Vernissage/Views/DetailsView.swift b/Vernissage/Views/DetailsView.swift index 7af6084..5265a17 100644 --- a/Vernissage/Views/DetailsView.swift +++ b/Vernissage/Views/DetailsView.swift @@ -9,18 +9,20 @@ import MastodonSwift struct DetailsView: View { @Environment(\.dismiss) private var dismiss - @State public var current: ImageStatus + @State public var statusData: StatusData var body: some View { ScrollView { VStack (alignment: .leading) { - Image(uiImage: current.image) - .resizable().aspectRatio(contentMode: .fit) - .frame(maxWidth: .infinity) + if let attachmentData = statusData.attachmentRelation?.first(where: { elemet in true}) as? AttachmentData { + Image(uiImage: UIImage(data: attachmentData.data)!) + .resizable().aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + } VStack(alignment: .leading) { HStack (alignment: .center) { - AsyncImage(url: current.status.account?.avatar) { image in + AsyncImage(url: statusData.accountAvatar) { image in image .resizable() .clipShape(Circle()) @@ -33,16 +35,16 @@ struct DetailsView: View { .frame(width: 48.0, height: 48.0) VStack (alignment: .leading) { - Text(current.status.account?.displayName ?? current.status.account?.username ?? "") + Text(statusData.accountDisplayName ?? statusData.accountUsername) .foregroundColor(Color("displayNameColor")) - Text("@\(current.status.account?.username ?? "unknown")") + Text("@\(statusData.accountUsername)") .foregroundColor(Color("lightGrayColor")) .font(.footnote) } .padding(.leading, 8) } - HTMLFormattedText(current.status.content) + HTMLFormattedText(statusData.content) VStack (alignment: .leading) { LabelIconView(iconName: "camera", value: "SONY ILCE-7M3") @@ -57,8 +59,8 @@ struct DetailsView: View { // Favorite } content: { HStack { - Image(systemName: current.status.favourited ? "heart.fill" : "heart") - Text("\(current.status.favouritesCount) likes") + Image(systemName: statusData.favourited ? "heart.fill" : "heart") + Text("\(statusData.favouritesCount) likes") } } @@ -66,8 +68,8 @@ struct DetailsView: View { // Reboost } content: { HStack { - Image(systemName: current.status.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward") - Text("\(current.status.reblogsCount) boosts") + Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward") + Text("\(statusData.reblogsCount) boosts") } } @@ -76,7 +78,7 @@ struct DetailsView: View { TagView { // Bookmark } content: { - Image(systemName: current.status.bookmarked ? "bookmark.fill" : "bookmark") + Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark") } } .font(.subheadline) diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index 741d076..a7b8be7 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -10,23 +10,26 @@ import MastodonSwift import UIKit struct HomeFeedView: View { + @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var applicationState: ApplicationState - @State private var statuses: [Status] = [] - @State private var images: [ImageStatus] = [] @State private var showLoading = false private static let initialColumns = 1 @State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns) + @FetchRequest(sortDescriptors: [SortDescriptor(\.id, order: .reverse)]) var dbStatuses: FetchedResults + var body: some View { ZStack { ScrollView { LazyVGrid(columns: gridColumns) { - ForEach(images) { item in - NavigationLink(destination: DetailsView(current: item)) { - Image(uiImage: item.image) - .resizable().aspectRatio(contentMode: .fit) + ForEach(dbStatuses) { item in + NavigationLink(destination: DetailsView(statusData: item)) { + if let attachmenData = item.attachmentRelation?.first(where: { element in true }) as? AttachmentData { + Image(uiImage: UIImage(data: attachmenData.data)!) + .resizable().aspectRatio(contentMode: .fit) + } } } } @@ -62,9 +65,11 @@ struct HomeFeedView: View { } .task { do { - self.showLoading = true - try await loadData() - self.showLoading = false + if self.dbStatuses.isEmpty { + self.showLoading = true + try await loadData() + self.showLoading = false + } } catch { self.showLoading = false print("Error", error) @@ -77,46 +82,88 @@ struct HomeFeedView: View { return } - let client = MastodonClient(baseURL: accessData.serverUrl).getAuthenticated(token: accessToken) - self.statuses = try await client.getHomeTimeline(limit: 40) + // Get maximimum downloaded stauts id. + let attachmentDataHandler = AttachmentDataHandler() + let statusDataHandler = StatusDataHandler() + let lastStatus = statusDataHandler.getMaximumStatus() - var imagesCache: [ImageStatus] = [] - for item in self.statuses { - let imageStatus = try await self.fetchImage(status: item) + // Retrieve statuses from API. + let client = MastodonClient(baseURL: accessData.serverUrl).getAuthenticated(token: accessToken) + let statuses = try await client.getHomeTimeline(minId: lastStatus?.id, limit: 40) + + // Download status images and save it into database. + for status in statuses { + + // Save status data in database. + let statusDataEntity = statusDataHandler.createStatusDataEntity() + statusDataEntity.accountAvatar = status.account?.avatar + statusDataEntity.accountDisplayName = status.account?.displayName + statusDataEntity.accountId = status.account!.id + statusDataEntity.accountUsername = status.account!.username + statusDataEntity.applicationName = status.application?.name + statusDataEntity.applicationWebsite = status.application?.website + statusDataEntity.bookmarked = status.bookmarked + statusDataEntity.content = status.content + statusDataEntity.createdAt = status.createdAt + statusDataEntity.favourited = status.favourited + statusDataEntity.favouritesCount = Int32(status.favouritesCount) + statusDataEntity.id = status.id + statusDataEntity.inReplyToAccount = status.inReplyToAccount + statusDataEntity.inReplyToId = status.inReplyToId + statusDataEntity.muted = status.muted + statusDataEntity.pinned = status.pinned + statusDataEntity.reblogged = status.reblogged + statusDataEntity.reblogsCount = Int32(status.reblogsCount) + statusDataEntity.sensitive = status.sensitive + statusDataEntity.spoilerText = status.spoilerText + statusDataEntity.uri = status.uri + statusDataEntity.url = status.url + statusDataEntity.visibility = status.visibility.rawValue + + for attachment in status.mediaAttachments { + let imageData = try await self.fetchImage(attachment: attachment) + + guard let imageData = imageData else { + continue + } + + /* + var exif = image.getExifData() + if let dict = exif as? [String: AnyObject] { + dict.keys.map { key in + print(key) + print(dict[key]) + } + } + */ + + // Save attachment in database. + let attachmentData = attachmentDataHandler.createAttachmnentDataEntity() + attachmentData.id = attachment.id + attachmentData.url = attachment.url + attachmentData.blurhash = attachment.blurhash + attachmentData.previewUrl = attachment.previewUrl + attachmentData.remoteUrl = attachment.remoteUrl + attachmentData.text = attachment.description + attachmentData.type = attachment.type.rawValue + + attachmentData.statusId = statusDataEntity.id + attachmentData.data = imageData - if let imageStatus { - imagesCache.append(imageStatus) + attachmentData.statusRelation = statusDataEntity + statusDataEntity.addToAttachmentRelation(attachmentData) } } - self.images = imagesCache + try self.viewContext.save() } - public func fetchImage(status: Status) async throws -> ImageStatus? { - guard let url = status.mediaAttachments.first?.url, let id = status.mediaAttachments.first?.id else { + public func fetchImage(attachment: Attachment) async throws -> Data? { + guard let data = try await RemoteFileService.shared.fetchData(url: attachment.url) else { return nil } - guard let data = try await RemoteFileService.shared.fetchData(url: url) else { - return nil - } - - let image = UIImage(data: data) - guard let image = image else { - return nil - } - - /* - var exif = image.getExifData() - if let dict = exif as? [String: AnyObject] { - dict.keys.map { key in - print(key) - print(dict[key]) - } - } - */ - - return ImageStatus(id: id,image: image, status: status) + return data } } diff --git a/Vernissage/Views/SignInView.swift b/Vernissage/Views/SignInView.swift index 2db3e98..067cd5e 100644 --- a/Vernissage/Views/SignInView.swift +++ b/Vernissage/Views/SignInView.swift @@ -29,67 +29,7 @@ struct SignInView: View { Button("Go") { Task { - let baseUrl = URL(string: serverAddress)! - let client = MastodonClient(baseURL: baseUrl) - - // Verify address. - let instanceInformation = try await client.readInstanceInformation() - print(instanceInformation) - - // Create application (we will get clientId amd clientSecret). - let oAuthApp = try await client.createApp(named: "Photofed", - redirectUri: "oauth-vernissage://oauth-callback/mastodon", - scopes: Scopes(["read", "write", "follow", "push"]), - website: baseUrl) - - // Authorize a user (browser, we will get clientCode). - let oAuthSwiftCredential = try await client.authenticate(app: oAuthApp, scope: Scopes(["read", "write", "follow", "push"])) - - // Get authenticated client. - let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken) - - // Get account information from server. - let account = try await authenticatedClient.verifyCredentials() - - // Create account object in database. - let accountDataHandler = AccountDataHandler() - let accountData = accountDataHandler.createAccountDataEntity() - - accountData.id = account.id - accountData.username = account.username - accountData.acct = account.acct - accountData.displayName = account.displayName - accountData.note = account.note - accountData.url = account.url - accountData.avatar = account.avatar - accountData.header = account.header - accountData.locked = account.locked - accountData.createdAt = account.createdAt - accountData.followersCount = Int32(account.followersCount) - accountData.followingCount = Int32(account.followingCount) - accountData.statusesCount = Int32(account.statusesCount) - - accountData.serverUrl = baseUrl - accountData.clientId = oAuthApp.clientId - accountData.clientSecret = oAuthApp.clientSecret - accountData.clientVapidKey = oAuthApp.vapidKey ?? "" - accountData.accessToken = oAuthSwiftCredential.oauthToken - - // Download avatar image. - if let avatarUrl = account.avatar { - do { - let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl) - accountData.avatarData = avatarData - } - catch { - print("Avatar has not been downloaded") - } - } - - // Save account data in database and in application state. - try self.viewContext.save() - self.applicationState.accountData = accountData - self.onSignInStateChenge(.mainView) + try await self.signIn() } } } @@ -98,11 +38,83 @@ struct SignInView: View { .navigationBarTitle("Sign in to Pixelfed") .navigationBarTitleDisplayMode(.inline) } + + private func signIn() async throws { + let baseUrl = URL(string: serverAddress)! + let client = MastodonClient(baseURL: baseUrl) + + // Verify address. + let instanceInformation = try await client.readInstanceInformation() + print(instanceInformation) + + // Create application (we will get clientId amd clientSecret). + let oAuthApp = try await client.createApp( + named: "Photofed", + redirectUri: "oauth-vernissage://oauth-callback/mastodon", + scopes: Scopes(["read", "write", "follow", "push"]), + website: baseUrl) + + // Authorize a user (browser, we will get clientCode). + let oAuthSwiftCredential = try await client.authenticate( + app: oAuthApp, + scope: Scopes(["read", "write", "follow", "push"])) + + // Get authenticated client. + let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken) + + // Get account information from server. + let account = try await authenticatedClient.verifyCredentials() + + // Create account object in database. + let accountDataHandler = AccountDataHandler() + let accountData = accountDataHandler.createAccountDataEntity() + + accountData.id = account.id + accountData.username = account.username + accountData.acct = account.acct + accountData.displayName = account.displayName + accountData.note = account.note + accountData.url = account.url + accountData.avatar = account.avatar + accountData.header = account.header + accountData.locked = account.locked + accountData.createdAt = account.createdAt + accountData.followersCount = Int32(account.followersCount) + accountData.followingCount = Int32(account.followingCount) + accountData.statusesCount = Int32(account.statusesCount) + + accountData.serverUrl = baseUrl + accountData.clientId = oAuthApp.clientId + accountData.clientSecret = oAuthApp.clientSecret + accountData.clientVapidKey = oAuthApp.vapidKey ?? "" + accountData.accessToken = oAuthSwiftCredential.oauthToken + + // Download avatar image. + if let avatarUrl = account.avatar { + do { + let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl) + accountData.avatarData = avatarData + } + catch { + print("Avatar has not been downloaded") + } + } + + // Set newly created account as current. + let applicationSettingsHandler = ApplicationSettingsHandler() + let defaultSettings = applicationSettingsHandler.getDefaultSettings() + defaultSettings.currentAccount = accountData.id + + // Save account data in database and in application state. + try self.viewContext.save() + + self.applicationState.accountData = accountData + self.onSignInStateChenge(.mainView) + } } struct SignInView_Previews: PreviewProvider { static var previews: some View { - SignInView { applicationViewMode in - } + SignInView { applicationViewMode in } } }