diff --git a/Assets/BlueLens.afdesign b/Assets/BlueLens.afdesign new file mode 100644 index 0000000..8635455 Binary files /dev/null and b/Assets/BlueLens.afdesign differ diff --git a/Assets/BrownLens.afdesign b/Assets/BrownLens.afdesign new file mode 100644 index 0000000..49e2431 Binary files /dev/null and b/Assets/BrownLens.afdesign differ diff --git a/Assets/Icons-Preview.afphoto b/Assets/Icons-Preview.afphoto index 8d6b024..44e1aa4 100644 Binary files a/Assets/Icons-Preview.afphoto and b/Assets/Icons-Preview.afphoto differ diff --git a/Assets/OrangeLens.afdesign b/Assets/OrangeLens.afdesign new file mode 100644 index 0000000..24c6f4e Binary files /dev/null and b/Assets/OrangeLens.afdesign differ diff --git a/Assets/PinkLens.afdesign b/Assets/PinkLens.afdesign new file mode 100644 index 0000000..5c4245c Binary files /dev/null and b/Assets/PinkLens.afdesign differ diff --git a/ClientKit/Sources/ClientKit/Client+Timeline.swift b/ClientKit/Sources/ClientKit/Client+Timeline.swift index 41b1383..e70305c 100644 --- a/ClientKit/Sources/ClientKit/Client+Timeline.swift +++ b/ClientKit/Sources/ClientKit/Client+Timeline.swift @@ -12,8 +12,9 @@ extension Client { public func getHomeTimeline(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil, - limit: Int = 40) async throws -> [Status] { - return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit) + limit: Int = 40, + includeReblogs: Bool? = nil) async throws -> [Status] { + return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit, includeReblogs: includeReblogs) } public func getStatuses(local: Bool? = nil, diff --git a/ClientKit/Sources/ClientKit/Models/StatusModel.swift b/ClientKit/Sources/ClientKit/Models/StatusModel.swift index 17f9252..ee21044 100644 --- a/ClientKit/Sources/ClientKit/Models/StatusModel.swift +++ b/ClientKit/Sources/ClientKit/Models/StatusModel.swift @@ -9,6 +9,7 @@ import PixelfedKit public class StatusModel: ObservableObject { public let id: EntityId + public let rebloggedStatusId: EntityId? public let content: Html public let uri: String? @@ -41,11 +42,12 @@ public class StatusModel: ObservableObject { @Published public var mediaAttachments: [AttachmentModel] public init(status: Status) { - + self.id = status.id + self.rebloggedStatusId = status.reblog?.id + // If status has been rebloged we are saving orginal status here. let orginalStatus = status.reblog ?? status - self.id = orginalStatus.id self.content = orginalStatus.content self.uri = orginalStatus.uri self.url = orginalStatus.url @@ -86,6 +88,13 @@ public class StatusModel: ObservableObject { } } +public extension StatusModel { + /// Function returns status Id for real status (status with images), even for reboosted statuses. + func getOrginalStatusId() -> EntityId { + return self.rebloggedStatusId ?? self.id + } +} + public extension StatusModel { func getImageWidth() -> Int32? { let highestImage = self.mediaAttachments.getHighestImage() diff --git a/CoreData/AccountData+CoreDataProperties.swift b/CoreData/AccountData+CoreDataProperties.swift index e17a638..e7252d2 100644 --- a/CoreData/AccountData+CoreDataProperties.swift +++ b/CoreData/AccountData+CoreDataProperties.swift @@ -34,6 +34,7 @@ extension AccountData { @NSManaged public var url: URL? @NSManaged public var username: String @NSManaged public var statuses: Set? + @NSManaged public var viewedStatuses: Set? @NSManaged public var lastSeenStatusId: String? } @@ -51,6 +52,18 @@ extension AccountData { @objc(removeStatuses:) @NSManaged public func removeFromStatuses(_ values: NSSet) + + @objc(addViewedStatusesObject:) + @NSManaged public func addToViewedStatuses(_ value: ViewedStatus) + + @objc(removeViewedStatusesObject:) + @NSManaged public func removeFromViewedStatuses(_ value: ViewedStatus) + + @objc(addViewedStatuses:) + @NSManaged public func addToViewedStatuses(_ values: NSSet) + + @objc(removeViewedStatuses:) + @NSManaged public func removeFromViewedStatuses(_ values: NSSet) } extension AccountData: Identifiable { diff --git a/CoreData/ApplicationSettings+CoreDataProperties.swift b/CoreData/ApplicationSettings+CoreDataProperties.swift index 0ecf351..1c9a977 100644 --- a/CoreData/ApplicationSettings+CoreDataProperties.swift +++ b/CoreData/ApplicationSettings+CoreDataProperties.swift @@ -34,6 +34,7 @@ extension ApplicationSettings { @NSManaged public var showAltIconOnTimeline: Bool @NSManaged public var warnAboutMissingAlt: Bool @NSManaged public var showGridOnUserProfile: Bool + @NSManaged public var showReboostedStatuses: Bool @NSManaged public var customNavigationMenuItem1: Int32 @NSManaged public var customNavigationMenuItem2: Int32 diff --git a/CoreData/ApplicationSettingsHandler.swift b/CoreData/ApplicationSettingsHandler.swift index edc87ea..a2e5541 100644 --- a/CoreData/ApplicationSettingsHandler.swift +++ b/CoreData/ApplicationSettingsHandler.swift @@ -59,6 +59,7 @@ class ApplicationSettingsHandler { applicationState.showAltIconOnTimeline = defaultSettings.showAltIconOnTimeline applicationState.warnAboutMissingAlt = defaultSettings.warnAboutMissingAlt applicationState.showGridOnUserProfile = defaultSettings.showGridOnUserProfile + applicationState.showReboostedStatuses = defaultSettings.showReboostedStatuses if let menuPosition = MenuPosition(rawValue: Int(defaultSettings.menuPosition)) { applicationState.menuPosition = menuPosition @@ -197,6 +198,12 @@ class ApplicationSettingsHandler { CoreDataHandler.shared.save() } + func set(showReboostedStatuses: Bool) { + let defaultSettings = self.get() + defaultSettings.showReboostedStatuses = showReboostedStatuses + CoreDataHandler.shared.save() + } + private func createApplicationSettingsEntity(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings { let context = viewContext ?? CoreDataHandler.shared.container.viewContext return ApplicationSettings(context: context) diff --git a/CoreData/CoreDataError.swift b/CoreData/CoreDataError.swift index 51ef570..0621347 100644 --- a/CoreData/CoreDataError.swift +++ b/CoreData/CoreDataError.swift @@ -5,12 +5,14 @@ // import Foundation +import OSLog +import EnvironmentKit public class CoreDataError { public static let shared = CoreDataError() private init() { } public func handle(_ error: Error, message: String) { - print("Error ['\(message)']: \(error.localizedDescription)") + Logger.main.error("Error ['\(message)']: \(error.localizedDescription)") } } diff --git a/CoreData/CoreDataHandler.swift b/CoreData/CoreDataHandler.swift index b7c8e7f..69df446 100644 --- a/CoreData/CoreDataHandler.swift +++ b/CoreData/CoreDataHandler.swift @@ -5,6 +5,7 @@ // import CoreData +import OSLog import EnvironmentKit public class CoreDataHandler { @@ -50,7 +51,7 @@ public class CoreDataHandler { do { try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType) } catch { - print(error.localizedDescription) + Logger.main.error("\(error.localizedDescription)") } // Delete old store. @@ -59,7 +60,7 @@ public class CoreDataHandler { do { try FileManager.default.removeItem(at: url) } catch { - print(error.localizedDescription) + Logger.main.error("\(error.localizedDescription)") } }) } diff --git a/CoreData/StatusData+Status.swift b/CoreData/StatusData+Status.swift index b0f7385..b8c4151 100644 --- a/CoreData/StatusData+Status.swift +++ b/CoreData/StatusData+Status.swift @@ -12,7 +12,9 @@ extension StatusData { if let reblog = status.reblog { self.copyFrom(reblog) - self.rebloggedStatusId = status.id + self.id = status.id + self.rebloggedStatusId = reblog.id + self.rebloggedAccountAvatar = status.account.avatar self.rebloggedAccountDisplayName = status.account.displayName self.rebloggedAccountId = status.account.id @@ -78,3 +80,9 @@ extension StatusData { } } } + +public extension StatusData { + func getOrginalStatusId() -> String { + return self.rebloggedStatusId ?? self.id + } +} diff --git a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion index a37b6e9..0ea4d11 100644 --- a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion +++ b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Vernissage-014.xcdatamodel + Vernissage-017.xcdatamodel diff --git a/CoreData/Vernissage.xcdatamodeld/Vernissage-015.xcdatamodel/contents b/CoreData/Vernissage.xcdatamodeld/Vernissage-015.xcdatamodel/contents new file mode 100644 index 0000000..f430ade --- /dev/null +++ b/CoreData/Vernissage.xcdatamodeld/Vernissage-015.xcdatamodel/contents @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CoreData/Vernissage.xcdatamodeld/Vernissage-016.xcdatamodel/contents b/CoreData/Vernissage.xcdatamodeld/Vernissage-016.xcdatamodel/contents new file mode 100644 index 0000000..5d15c95 --- /dev/null +++ b/CoreData/Vernissage.xcdatamodeld/Vernissage-016.xcdatamodel/contents @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CoreData/Vernissage.xcdatamodeld/Vernissage-017.xcdatamodel/contents b/CoreData/Vernissage.xcdatamodeld/Vernissage-017.xcdatamodel/contents new file mode 100644 index 0000000..14da0a3 --- /dev/null +++ b/CoreData/Vernissage.xcdatamodeld/Vernissage-017.xcdatamodel/contents @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CoreData/ViewedStatus+CoreDataClass.swift b/CoreData/ViewedStatus+CoreDataClass.swift new file mode 100644 index 0000000..5b306b1 --- /dev/null +++ b/CoreData/ViewedStatus+CoreDataClass.swift @@ -0,0 +1,12 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import CoreData + +@objc(ViewedStatus) +public class ViewedStatus: NSManagedObject { +} diff --git a/CoreData/ViewedStatus+CoreDataProperties.swift b/CoreData/ViewedStatus+CoreDataProperties.swift new file mode 100644 index 0000000..e3fe561 --- /dev/null +++ b/CoreData/ViewedStatus+CoreDataProperties.swift @@ -0,0 +1,23 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import CoreData + +extension ViewedStatus { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "ViewedStatus") + } + + @NSManaged public var id: String + @NSManaged public var reblogId: String? + @NSManaged public var date: Date + @NSManaged public var pixelfedAccount: AccountData +} + +extension ViewedStatus: Identifiable { +} diff --git a/CoreData/ViewedStatusHandler.swift b/CoreData/ViewedStatusHandler.swift new file mode 100644 index 0000000..0c15d37 --- /dev/null +++ b/CoreData/ViewedStatusHandler.swift @@ -0,0 +1,82 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import CoreData +import PixelfedKit + +class ViewedStatusHandler { + public static let shared = ViewedStatusHandler() + private init() { } + + func createViewedStatusEntity(viewContext: NSManagedObjectContext? = nil) -> ViewedStatus { + let context = viewContext ?? CoreDataHandler.shared.container.viewContext + return ViewedStatus(context: context) + } + + /// Check if given status (real picture) has been already visible on the timeline (during last month). + func hasBeenAlreadyOnTimeline(accountId: String, status: Status, viewContext: NSManagedObjectContext? = nil) -> Bool { + guard let reblog = status.reblog else { + return false + } + + let context = viewContext ?? CoreDataHandler.shared.container.viewContext + let fetchRequest = ViewedStatus.fetchRequest() + + fetchRequest.fetchLimit = 1 + let statusIdPredicate = NSPredicate(format: "id = %@", reblog.id) + let reblogIdPredicate = NSPredicate(format: "reblogId = %@", reblog.id) + let idPredicates = NSCompoundPredicate.init(type: .or, subpredicates: [statusIdPredicate, reblogIdPredicate]) + + let accountPredicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId) + fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [idPredicates, accountPredicate]) + + do { + guard let first = try context.fetch(fetchRequest).first else { + return false + } + + if first.reblogId == nil { + return true + } + + if first.id != status.id { + return true + } + + return false + } catch { + CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (hasBeenAlreadyOnTimeline).") + return false + } + } + + /// Mark to delete statuses older then one month. + func deleteOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) { + let oldViewedStatuses = self.getOldViewedStatuses(viewContext: viewContext) + for status in oldViewedStatuses { + viewContext?.delete(status) + } + } + + private func getOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) -> [ViewedStatus] { + let context = viewContext ?? CoreDataHandler.shared.container.viewContext + + guard let date = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { + return [] + } + + do { + let fetchRequest = ViewedStatus.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "date < %@", date as NSDate) + + return try context.fetch(fetchRequest) + } catch { + CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (getOldViewedStatuses).") + return [] + } + } +} diff --git a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift index dbdfbb2..9049cef 100644 --- a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift +++ b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift @@ -107,6 +107,9 @@ public class ApplicationState: ObservableObject { /// Show grid of photos on user profile. @Published public var showGridOnUserProfile = false + /// Show reboosted statuses on home timeline. + @Published public var showReboostedStatuses = false + public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) { self.account = accountModel self.lastSeenStatusId = lastSeenStatusId diff --git a/EnvironmentKit/Sources/EnvironmentKit/Extensions/OSLog+Logger.swift b/EnvironmentKit/Sources/EnvironmentKit/Extensions/OSLog+Logger.swift new file mode 100644 index 0000000..b0bb136 --- /dev/null +++ b/EnvironmentKit/Sources/EnvironmentKit/Extensions/OSLog+Logger.swift @@ -0,0 +1,15 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import OSLog + +public extension Logger { + /// Using your bundle identifier is a great way to ensure a unique identifier. + private static var subsystem = Bundle.main.bundleIdentifier ?? "dev.mczachurski.vernissage" + + /// Logs the main informations. + static let main = Logger(subsystem: subsystem, category: "main") +} diff --git a/Localization/en.lproj/Localizable.strings b/Localization/en.lproj/Localizable.strings index 311fdda..d404952 100644 --- a/Localization/en.lproj/Localizable.strings +++ b/Localization/en.lproj/Localizable.strings @@ -240,6 +240,8 @@ "settings.title.showAltTextOnTimeline" = "ALT icon will be displayed on timelines"; "settings.title.warnAboutMissingAltTitle" = "Warn of missing ALT text"; "settings.title.warnAboutMissingAltDescription" = "A warning about missing ALT texts will be displayed before publishing new post."; +"settings.title.enableReboostOnTimeline" = "Show boosted statuses"; +"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline."; // Mark: Signin view. "signin.navigationBar.title" = "Sign in to Pixelfed"; diff --git a/Localization/eu.lproj/Localizable.strings b/Localization/eu.lproj/Localizable.strings index 7363d4d..8dbe06c 100644 --- a/Localization/eu.lproj/Localizable.strings +++ b/Localization/eu.lproj/Localizable.strings @@ -240,6 +240,8 @@ "settings.title.showAltTextOnTimeline" = "ALT ikurra (deskribapena edo testu alternatiboa dagoenaren seinale) denbora-lerroan erakutsiko da"; "settings.title.warnAboutMissingAltTitle" = "Abisatu ALT ahaztu bazait"; "settings.title.warnAboutMissingAltDescription" = "Irudiren batek deskribapenik ez badu, argitaratu baino lehen abisua erakutsiko da."; +"settings.title.enableReboostOnTimeline" = "Show boosted statuses"; +"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline."; // Mark: Signin view. "signin.navigationBar.title" = "Hasi saioa Pixelfed-en"; diff --git a/Localization/fr.lproj/Localizable.strings b/Localization/fr.lproj/Localizable.strings index 0718e55..472244d 100644 --- a/Localization/fr.lproj/Localizable.strings +++ b/Localization/fr.lproj/Localizable.strings @@ -240,6 +240,8 @@ "settings.title.showAltTextOnTimeline" = "L'icône ALT sera affichée sur la timeline"; "settings.title.warnAboutMissingAltTitle" = "Avertir de l'absence de texte ALT"; "settings.title.warnAboutMissingAltDescription" = "Un avertissement concernant les textes ALT manquants sera affiché avant la publication d'un nouveau message."; +"settings.title.enableReboostOnTimeline" = "Show boosted statuses"; +"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline."; // Mark: Signin view. "signin.navigationBar.title" = "Se connecter à Pixelfed"; diff --git a/Localization/pl.lproj/Localizable.strings b/Localization/pl.lproj/Localizable.strings index 245aa82..351a159 100644 --- a/Localization/pl.lproj/Localizable.strings +++ b/Localization/pl.lproj/Localizable.strings @@ -240,6 +240,8 @@ "settings.title.showAltTextOnTimeline" = "Ikony ALT będą widonczne na osiach zdjęć"; "settings.title.warnAboutMissingAltTitle" = "Ostrzeganie o brakującym tekście ALT"; "settings.title.warnAboutMissingAltDescription" = "Ostrzeżenie o brakujących tekstach ALT będzie wyświetlane przed opublikowaniem nowego statusu."; +"settings.title.enableReboostOnTimeline" = "Wyświetl podbite statusy"; +"settings.title.enableReboostOnTimelineDescription" = "Podbite statusy będą widoczne na twojej osi czasu."; // Mark: Signin view. "signin.navigationBar.title" = "Zaloguj się do Pixelfed"; diff --git a/WidgetsKit/Sources/WidgetsKit/Extensions/String+Random.swift b/PixelfedKit/Sources/PixelfedKit/Extensions/String+Random.swift similarity index 100% rename from WidgetsKit/Sources/WidgetsKit/Extensions/String+Random.swift rename to PixelfedKit/Sources/PixelfedKit/Extensions/String+Random.swift diff --git a/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Timelines.swift b/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Timelines.swift index fcd0127..1d387b8 100644 --- a/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Timelines.swift +++ b/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Timelines.swift @@ -11,11 +11,12 @@ public extension PixelfedClientAuthenticated { maxId: EntityId? = nil, sinceId: EntityId? = nil, minId: EntityId? = nil, - limit: Int? = nil) async throws -> [Status] { + limit: Int? = nil, + includeReblogs: Bool? = nil) async throws -> [Status] { let request = try Self.request( for: baseURL, - target: Pixelfed.Timelines.home(maxId, sinceId, minId, limit), + target: Pixelfed.Timelines.home(maxId, sinceId, minId, limit, includeReblogs), withBearerToken: token ) diff --git a/PixelfedKit/Sources/PixelfedKit/Targets/Timelines.swift b/PixelfedKit/Sources/PixelfedKit/Targets/Timelines.swift index b7cbfad..c1966f3 100644 --- a/PixelfedKit/Sources/PixelfedKit/Targets/Timelines.swift +++ b/PixelfedKit/Sources/PixelfedKit/Targets/Timelines.swift @@ -8,7 +8,7 @@ import Foundation extension Pixelfed { public enum Timelines { - case home(MaxId?, SinceId?, MinId?, Limit?) + case home(MaxId?, SinceId?, MinId?, Limit?, Bool?) case pub(Bool?, Bool?, Bool?, MaxId?, SinceId?, MinId?, Limit?) case tag(String, Bool?, Bool?, Bool?, MaxId?, SinceId?, MinId?, Limit?) } @@ -43,6 +43,7 @@ extension Pixelfed.Timelines: TargetType { var local: Bool? var remote: Bool? var onlyMedia: Bool? + var includeReblogs: Bool? var maxId: MaxId? var sinceId: SinceId? var minId: MinId? @@ -58,34 +59,48 @@ extension Pixelfed.Timelines: TargetType { sinceId = paramSinceId minId = paramMinId limit = paramLimit - case .home(let paramMaxId, let paramSinceId, let paramMinId, let paramLimit): + case .home(let paramMaxId, let paramSinceId, let paramMinId, let paramLimit, let paramIncludeReblogs): maxId = paramMaxId sinceId = paramSinceId minId = paramMinId limit = paramLimit + includeReblogs = paramIncludeReblogs } if let maxId { params.append(("max_id", maxId)) } + if let sinceId { params.append(("since_id", sinceId)) } + if let minId { params.append(("min_id", minId)) } + if let limit { params.append(("limit", "\(limit)")) } + if let local { params.append(("local", local.asString)) } + if let remote { params.append(("remote", remote.asString)) } + if let onlyMedia { params.append(("only_media", onlyMedia.asString)) } + + if let includeReblogs, includeReblogs == true { + params.append(("include_reblogs", includeReblogs.asString)) + } + + params.append(("_t", String.randomString(length: 8))) + return params } diff --git a/ServicesKit/Sources/ServicesKit/ErrorsService.swift b/ServicesKit/Sources/ServicesKit/ErrorsService.swift index 14bb60f..27c8229 100644 --- a/ServicesKit/Sources/ServicesKit/ErrorsService.swift +++ b/ServicesKit/Sources/ServicesKit/ErrorsService.swift @@ -5,6 +5,8 @@ // import Foundation +import OSLog +import EnvironmentKit import PixelfedKit public class ErrorService { @@ -23,6 +25,7 @@ public class ErrorService { } } - print("Error ['\(localizedMessage)']: \(error.localizedDescription)") + Logger.main.error("Error ['\(localizedMessage)']: \(error.localizedDescription)") + Logger.main.error("Error ['\(localizedMessage)']: \(error)") } } diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index e6848eb..b485c88 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -194,6 +194,15 @@ F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; }; F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B758DD2AB9DD85000C8068 /* ColumnData.swift */; }; F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; }; + F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; }; + F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; }; + F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; }; + F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; }; + F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; }; + F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; }; + F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; }; + F8D8E0D02ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; }; + F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; }; F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; }; F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; }; F8E36E462AB8745300769C55 /* Sizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E452AB8745300769C55 /* Sizable.swift */; }; @@ -332,6 +341,7 @@ F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMetadataService.swift; sourceTree = ""; }; F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSession.swift; sourceTree = ""; }; F87AEB962986D16D00434FB6 /* AuthorisationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorisationError.swift; sourceTree = ""; }; + F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-015.xcdatamodel"; sourceTree = ""; }; F883401F29B62AE900C3E096 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; F88AB05229B3613900345EDE /* PhotoUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoUrl.swift; sourceTree = ""; }; F88AB05429B3626300345EDE /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = ""; }; @@ -398,9 +408,14 @@ F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.swift; sourceTree = ""; }; + F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-016.xcdatamodel"; sourceTree = ""; }; F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = ""; }; F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = ""; }; F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataClass.swift"; sourceTree = ""; }; + F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataProperties.swift"; sourceTree = ""; }; + F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatusHandler.swift; sourceTree = ""; }; + F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-017.xcdatamodel"; sourceTree = ""; }; F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = ""; }; F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = ""; }; F8DF38E729DDC3D20047F1AA /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; @@ -569,11 +584,14 @@ F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */, F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */, F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */, + F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */, + F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */, F88C2474295C37BB0006098B /* CoreDataHandler.swift */, F866F6A229604161002E8F88 /* AccountDataHandler.swift */, F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */, F80048072961E6DE00E6868A /* StatusDataHandler.swift */, F80048092961EA1900E6868A /* AttachmentDataHandler.swift */, + F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */, F864F7A429BBA01D00B13921 /* CoreDataError.swift */, ); path = CoreData; @@ -1098,6 +1116,7 @@ files = ( F864F77829BB930000B13921 /* PhotoWidgetEntry.swift in Sources */, F864F77529BB92CE00B13921 /* PhotoProvider.swift in Sources */, + F8D8E0D02ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */, F864F77629BB92CE00B13921 /* PhotoWidgetEntryView.swift in Sources */, F8705A7729FF7ABD00DA818A /* QRCodeSmallWidgetView.swift in Sources */, F864F77C29BB982100B13921 /* StatusFetcher.swift in Sources */, @@ -1110,6 +1129,8 @@ F8F6E44E29BCC1FB0004795E /* PhotoLargeWidgetView.swift in Sources */, F864F76429BB91B400B13921 /* VernissageWidgetBundle.swift in Sources */, F864F77D29BB9A4600B13921 /* AttachmentData+CoreDataClass.swift in Sources */, + F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */, + F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */, F864F7A629BBA01D00B13921 /* CoreDataError.swift in Sources */, F864F77E29BB9A4900B13921 /* AttachmentData+CoreDataProperties.swift in Sources */, F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */, @@ -1144,6 +1165,7 @@ files = ( F88BC54529E072B200CE6141 /* AccountDataHandler.swift in Sources */, F88BC54729E072B800CE6141 /* AccountData+CoreDataProperties.swift in Sources */, + F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */, F88BC54D29E072D600CE6141 /* AttachmentData+CoreDataProperties.swift in Sources */, F88BC54F29E073BC00CE6141 /* AccountData+AccountModel.swift in Sources */, F865B4D32A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */, @@ -1152,10 +1174,12 @@ F88BC54129E072A600CE6141 /* CoreDataError.swift in Sources */, F88BC54229E072A900CE6141 /* AttachmentDataHandler.swift in Sources */, F88BC54429E072AF00CE6141 /* ApplicationSettingsHandler.swift in Sources */, + F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */, F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */, F88BC54829E072BC00CE6141 /* AccountData+CoreDataClass.swift in Sources */, F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */, F88BC54E29E072D900CE6141 /* AttachmentData+CoreDataClass.swift in Sources */, + F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */, F88BC54C29E072CD00CE6141 /* StatusData+CoreDataClass.swift in Sources */, F88BC54B29E072CA00CE6141 /* StatusData+CoreDataProperties.swift in Sources */, F88BC54A29E072C400CE6141 /* ApplicationSettings+CoreDataClass.swift in Sources */, @@ -1198,6 +1222,7 @@ F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */, F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */, F808641429756666009F035C /* NotificationRowView.swift in Sources */, + F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */, F8624D3D29F2D3AC00204986 /* SelectedMenuItemDetails.swift in Sources */, F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */, F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */, @@ -1252,6 +1277,7 @@ F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */, F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */, F8FB8ABA29EB2ED400342C04 /* NavigationMenuButtons.swift in Sources */, + F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */, F88BC51D29E0377B00CE6141 /* AccountData+AccountModel.swift in Sources */, F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */, F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */, @@ -1264,6 +1290,7 @@ F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */, F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */, F86B7216296BFFDA00EE59EC /* UserProfileStatusesView.swift in Sources */, + F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */, F897978F29684BCB00B22335 /* LoadingView.swift in Sources */, F89992C9296D6DC7005994BF /* CommentBodyView.swift in Sources */, F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */, @@ -1321,7 +1348,7 @@ CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1333,7 +1360,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.0; + MARKETING_VERSION = 1.12.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1352,7 +1379,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1364,7 +1391,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.0; + MARKETING_VERSION = 1.12.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1382,7 +1409,7 @@ CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1394,7 +1421,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.0; + MARKETING_VERSION = 1.12.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1411,7 +1438,7 @@ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1423,7 +1450,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.0; + MARKETING_VERSION = 1.12.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1557,7 +1584,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient"; + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient Brown-Lens Pink-Lens Blue-Lens Orange-Lens"; ASSETCATALOG_COMPILER_APPICON_NAME = Default; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; @@ -1565,7 +1592,7 @@ CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1584,13 +1611,13 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.11.0; + MARKETING_VERSION = 1.12.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1601,14 +1628,14 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient"; + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient Brown-Lens Pink-Lens Blue-Lens Orange-Lens"; ASSETCATALOG_COMPILER_APPICON_NAME = Default; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1627,12 +1654,12 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.11.0; + MARKETING_VERSION = 1.12.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1805,6 +1832,9 @@ F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */, + F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */, + F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */, F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */, F865B4D42A0252FB008ACDFC /* Vernissage-013.xcdatamodel */, F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */, @@ -1821,7 +1851,7 @@ F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */, F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */, ); - currentVersion = F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */; + currentVersion = F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */; path = Vernissage.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview.png b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview.png new file mode 100644 index 0000000..7ab9cf2 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@2x.png b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@2x.png new file mode 100644 index 0000000..28231ff Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@2x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@3x.png b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@3x.png new file mode 100644 index 0000000..3dd8105 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@3x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Contents.json new file mode 100644 index 0000000..1128093 --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Blue-Lens-Preview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Blue-Lens-Preview@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Blue-Lens-Preview@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Blue-Lens.appiconset/BlueLens.png b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens.appiconset/BlueLens.png new file mode 100644 index 0000000..8f40abc Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens.appiconset/BlueLens.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Blue-Lens.appiconset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens.appiconset/Contents.json new file mode 100644 index 0000000..ca06826 --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Blue-Lens.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "BlueLens.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview.png b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview.png new file mode 100644 index 0000000..e703b38 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@2x.png b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@2x.png new file mode 100644 index 0000000..9bb314d Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@2x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@3x.png b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@3x.png new file mode 100644 index 0000000..c3cd718 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@3x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Contents.json new file mode 100644 index 0000000..edc60f2 --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Brown-Lens-Preview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Brown-Lens-Preview@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Brown-Lens-Preview@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Brown-Lens.appiconset/BrownLens.png b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens.appiconset/BrownLens.png new file mode 100644 index 0000000..ba167c6 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens.appiconset/BrownLens.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Brown-Lens.appiconset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens.appiconset/Contents.json new file mode 100644 index 0000000..422756e --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Brown-Lens.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "BrownLens.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Contents.json new file mode 100644 index 0000000..4ca7cd4 --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Lens-Preview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Lens-Preview@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Lens-Preview@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview.png b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview.png new file mode 100644 index 0000000..9ae0004 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@2x.png b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@2x.png new file mode 100644 index 0000000..30c671a Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@2x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@3x.png b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@3x.png new file mode 100644 index 0000000..80d39d4 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@3x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Orange-Lens.appiconset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens.appiconset/Contents.json new file mode 100644 index 0000000..8ac8d6e --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Lens.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Orange-Lens.appiconset/Lens.png b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens.appiconset/Lens.png new file mode 100644 index 0000000..c9d1199 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Orange-Lens.appiconset/Lens.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Contents.json new file mode 100644 index 0000000..d421c8e --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Pink-Lens-Preview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Pink-Lens-Preview@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Pink-Lens-Preview@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview.png b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview.png new file mode 100644 index 0000000..6386a9f Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@2x.png b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@2x.png new file mode 100644 index 0000000..3a081b6 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@2x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@3x.png b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@3x.png new file mode 100644 index 0000000..937dac6 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@3x.png differ diff --git a/Vernissage/Assets.xcassets/AppIcons/Pink-Lens.appiconset/Contents.json b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens.appiconset/Contents.json new file mode 100644 index 0000000..fcc6743 --- /dev/null +++ b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "PinkLens.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/AppIcons/Pink-Lens.appiconset/PinkLens.png b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens.appiconset/PinkLens.png new file mode 100644 index 0000000..9bcafeb Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcons/Pink-Lens.appiconset/PinkLens.png differ diff --git a/Vernissage/EnvironmentObjects/TipsStore.swift b/Vernissage/EnvironmentObjects/TipsStore.swift index 190a4c2..e8c77b7 100644 --- a/Vernissage/EnvironmentObjects/TipsStore.swift +++ b/Vernissage/EnvironmentObjects/TipsStore.swift @@ -7,6 +7,8 @@ import Foundation import StoreKit import ServicesKit +import OSLog +import EnvironmentKit @MainActor final class TipsStore: ObservableObject { @@ -78,9 +80,9 @@ final class TipsStore: ObservableObject { self.status = .successful await transaction.finish() case .userCancelled: - print("User click cancel before their transaction started.") + Logger.main.warning("User click cancel before their transaction started.") case .pending: - print("User needs to complete some action on their account before their complete the purchase.") + Logger.main.warning("User needs to complete some action on their account before their complete the purchase.") default: break } diff --git a/Vernissage/Services/HomeTimelineService.swift b/Vernissage/Services/HomeTimelineService.swift index a2b78c9..cb5e33b 100644 --- a/Vernissage/Services/HomeTimelineService.swift +++ b/Vernissage/Services/HomeTimelineService.swift @@ -10,6 +10,9 @@ import PixelfedKit import ClientKit import ServicesKit import Nuke +import OSLog +import EnvironmentKit +import Semaphore /// Service responsible for managing home timeline. public class HomeTimelineService { @@ -18,9 +21,10 @@ public class HomeTimelineService { private let defaultAmountOfDownloadedStatuses = 40 private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) + private let semaphore = AsyncSemaphore(value: 1) @MainActor - public func loadOnBottom(for account: AccountModel) async throws -> Int { + public func loadOnBottom(for account: AccountModel, includeReblogs: Bool) async throws -> Int { // Load data from API and operate on CoreData on background context. let backgroundContext = CoreDataHandler.shared.newBackgroundContext() @@ -32,7 +36,7 @@ public class HomeTimelineService { } // Load data on bottom of the list. - let allStatusesFromApi = try await self.load(for: account, on: backgroundContext, maxId: oldestStatus.id) + let allStatusesFromApi = try await self.load(for: account, includeReblogs: includeReblogs, on: backgroundContext, maxId: oldestStatus.id) // Save data into database. CoreDataHandler.shared.save(viewContext: backgroundContext) @@ -45,26 +49,32 @@ public class HomeTimelineService { } @MainActor - public func refreshTimeline(for account: AccountModel, updateLastSeenStatus: Bool = false) async throws -> String? { + public func refreshTimeline(for account: AccountModel, includeReblogs: Bool, updateLastSeenStatus: Bool = false) async throws -> String? { + await semaphore.wait() + defer { semaphore.signal() } + // 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 + let lastSeenStatusId = 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 allStatusesFromApi = try await self.refresh(for: account, on: backgroundContext) + let allStatusesFromApi = try await self.refresh(for: account, includeReblogs: includeReblogs, on: backgroundContext) // Update last seen status. if let lastSeenStatusId, updateLastSeenStatus == true { try self.update(lastSeenStatusId: lastSeenStatusId, for: account, on: backgroundContext) } + // Delete old viewed statuses from database. + ViewedStatusHandler.shared.deleteOldViewedStatuses(viewContext: backgroundContext) + // Start prefetching images. self.prefetch(statuses: allStatusesFromApi) - + // Save data into database. CoreDataHandler.shared.save(viewContext: backgroundContext) @@ -72,28 +82,6 @@ public class HomeTimelineService { return lastSeenStatusId } - private func update(lastSeenStatusId: String, for account: AccountModel, on backgroundContext: NSManagedObjectContext) throws { - // Save information about last seen status. - guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else { - throw DatabaseError.cannotDownloadAccount - } - - accountDataFromDb.lastSeenStatusId = lastSeenStatusId - } - - public func update(status statusData: StatusData, basedOn status: Status, for account: AccountModel) async throws -> StatusData? { - // Load data from API and operate on CoreData on background context. - let backgroundContext = CoreDataHandler.shared.newBackgroundContext() - - // Update status data in database. - self.copy(from: status, to: statusData, on: backgroundContext) - - // Save data into database. - CoreDataHandler.shared.save(viewContext: backgroundContext) - - return statusData - } - @MainActor public func update(attachment: AttachmentData, withData imageData: Data, imageWidth: Double, imageHeight: Double) { attachment.data = imageData @@ -107,7 +95,10 @@ public class HomeTimelineService { CoreDataHandler.shared.save() } - public func amountOfNewStatuses(for account: AccountModel) async -> Int { + public func amountOfNewStatuses(for account: AccountModel, includeReblogs: Bool) async -> Int { + await semaphore.wait() + defer { semaphore.signal() } + guard let accessToken = account.accessToken else { return 0 } @@ -122,13 +113,16 @@ public class HomeTimelineService { } let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken) - var amountOfStatuses = 0 + var statuses: [Status] = [] var newestStatusId = newestStatus.id // There can be more then 40 newest statuses, that's why we have to sometimes send more then one request. while true { do { - let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId, limit: self.defaultAmountOfDownloadedStatuses) + let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId, + limit: self.defaultAmountOfDownloadedStatuses, + includeReblogs: includeReblogs) + guard let firstStatus = downloadedStatuses.first else { break } @@ -136,25 +130,64 @@ public class HomeTimelineService { // We have to include in the counter only statuses with images. let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly() - amountOfStatuses = amountOfStatuses + statusesWithImagesOnly.count + for status in statusesWithImagesOnly { + // We should add to timeline only statuses that has not been showned to the user already. + guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else { + continue + } + + // Same rebloged status has been already visible in current portion of data. + if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) { + continue + } + + // Same status has been already visible in current portion of data. + if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) { + continue + } + + statuses.append(status) + } + newestStatusId = firstStatus.id } catch { ErrorService.shared.handle(error, message: "Error during downloading new statuses for amount of new statuses.") break } } + + // Start prefetching images. + self.prefetch(statuses: statuses) - return amountOfStatuses + // Return number of new statuses not visible yet on the timeline. + return statuses.count } - private func refresh(for account: AccountModel, on backgroundContext: NSManagedObjectContext) async throws -> [Status] { - guard let accessToken = account.accessToken else { - return [] + private func update(lastSeenStatusId: String, for account: AccountModel, on backgroundContext: NSManagedObjectContext) throws { + // Save information about last seen status. + guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else { + throw DatabaseError.cannotDownloadAccount } + accountDataFromDb.lastSeenStatusId = lastSeenStatusId + } + + private func update(status statusData: StatusData, basedOn status: Status, for account: AccountModel) async throws -> StatusData? { + // Load data from API and operate on CoreData on background context. + let backgroundContext = CoreDataHandler.shared.newBackgroundContext() + + // Update status data in database. + self.copy(from: status, to: statusData, on: backgroundContext) + + // Save data into database. + CoreDataHandler.shared.save(viewContext: backgroundContext) + + return statusData + } + + private func refresh(for account: AccountModel, includeReblogs: Bool, on backgroundContext: NSManagedObjectContext) async throws -> [Status] { // Retrieve statuses from API. - let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken) - let statuses = try await client.getHomeTimeline(limit: self.defaultAmountOfDownloadedStatuses) + let statuses = try await self.getUniqueStatusesForHomeTimeline(account: account, includeReblogs: includeReblogs, on: backgroundContext) // Update all existing statuses in database. for status in statuses { @@ -207,17 +240,12 @@ public class HomeTimelineService { } private func load(for account: AccountModel, + includeReblogs: Bool, on backgroundContext: NSManagedObjectContext, - minId: String? = nil, maxId: String? = nil ) async throws -> [Status] { - guard let accessToken = account.accessToken else { - return [] - } - // Retrieve statuses from API. - let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken) - let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: self.defaultAmountOfDownloadedStatuses) + let statuses = try await self.getUniqueStatusesForHomeTimeline(account: account, maxId: maxId, includeReblogs: includeReblogs, on: backgroundContext) // Save statuses in database. try await self.add(statuses, for: account, on: backgroundContext) @@ -238,14 +266,23 @@ public class HomeTimelineService { // Proceed statuses with images only. let statusesWithImages = statuses.getStatusesWithImagesOnly() - // Save status data in database. + // Save all data to database. for status in statusesWithImages { + // Save status to database. let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext) - + self.copy(from: status, to: statusData, on: backgroundContext) + statusData.pixelfedAccount = accountDataFromDb accountDataFromDb.addToStatuses(statusData) - self.copy(from: status, to: statusData, on: backgroundContext) + // Save statusId to viewed statuses. + let viewedStatus = ViewedStatusHandler.shared.createViewedStatusEntity(viewContext: backgroundContext) + + viewedStatus.id = status.id + viewedStatus.reblogId = status.reblog?.id + viewedStatus.date = Date() + viewedStatus.pixelfedAccount = accountDataFromDb + accountDataFromDb.addToViewedStatuses(viewedStatus) } } @@ -298,7 +335,62 @@ public class HomeTimelineService { } private func prefetch(statuses: [Status]) { - let statusModels = statuses.getStatusesWithImagesOnly().toStatusModels() + let statusModels = statuses.toStatusModels() imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls()) } + + private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, on backgroundContext: NSManagedObjectContext) -> Bool { + return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, viewContext: backgroundContext) + } + + private func getUniqueStatusesForHomeTimeline(account: AccountModel, maxId: EntityId? = nil, includeReblogs: Bool? = nil, on backgroundContext: NSManagedObjectContext) async throws -> [Status] { + guard let accessToken = account.accessToken else { + return [] + } + + let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken) + var lastStatusId = maxId + var statuses: [Status] = [] + + while true { + let downloadedStatuses = try await client.getHomeTimeline(maxId: lastStatusId, + limit: self.defaultAmountOfDownloadedStatuses, + includeReblogs: includeReblogs) + + // When there is not any older statuses we have to finish. + guard let lastStatus = downloadedStatuses.last else { + break + } + + // We have to include in the counter only statuses with images. + let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly() + + for status in statusesWithImagesOnly { + // We should add to timeline only statuses that has not been showned to the user already. + guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else { + continue + } + + // Same rebloged status has been already visible in current portion of data. + if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) { + continue + } + + // Same status has been already visible in current portion of data. + if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) { + continue + } + + statuses.append(status) + } + + if statuses.count >= self.defaultAmountOfDownloadedStatuses { + break + } + + lastStatusId = lastStatus.id + } + + return statuses + } } diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index d4e63c8..75a7d49 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -204,7 +204,8 @@ struct VernissageApp: App { private func calculateNewPhotosInBackground() async { if let account = self.applicationState.account { - self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account) + self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account, + includeReblogs: self.applicationState.showReboostedStatuses) } } } diff --git a/Vernissage/ViewModifiers/ImageContextMenu.swift b/Vernissage/ViewModifiers/ImageContextMenu.swift index f0af4fa..16da9a1 100644 --- a/Vernissage/ViewModifiers/ImageContextMenu.swift +++ b/Vernissage/ViewModifiers/ImageContextMenu.swift @@ -11,11 +11,25 @@ import ServicesKit public extension View { func imageContextMenu(statusModel: StatusModel, attachmentModel: AttachmentModel, uiImage: UIImage?) -> some View { - modifier(ImageContextMenu(id: statusModel.id, url: statusModel.url, altText: attachmentModel.description, uiImage: uiImage)) + modifier( + ImageContextMenu( + id: statusModel.getOrginalStatusId(), + url: statusModel.url, + altText: attachmentModel.description, + uiImage: uiImage + ) + ) } func imageContextMenu(statusData: StatusData, attachmentData: AttachmentData, uiImage: UIImage?) -> some View { - modifier(ImageContextMenu(id: statusData.id, url: statusData.url, altText: attachmentData.text, uiImage: uiImage)) + modifier( + ImageContextMenu( + id: statusData.getOrginalStatusId(), + url: statusData.url, + altText: attachmentData.text, + uiImage: uiImage + ) + ) } } diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index b79623d..beaf707 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -8,6 +8,8 @@ import SwiftUI import ServicesKit import EnvironmentKit import WidgetsKit +import OSLog +import Semaphore struct HomeFeedView: View { @Environment(\.managedObjectContext) private var viewContext @@ -69,7 +71,7 @@ struct HomeFeedView: View { .task { do { if let account = self.applicationState.account { - let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account) + let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account, includeReblogs: self.applicationState.showReboostedStatuses) if newStatusesCount == 0 { allItemsLoaded = true } @@ -101,9 +103,8 @@ struct HomeFeedView: View { private func refreshData() async { do { if let account = self.applicationState.account { - let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account, updateLastSeenStatus: true) - - asyncAfter(0.35) { + let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account, includeReblogs: self.applicationState.showReboostedStatuses, updateLastSeenStatus: true) + asyncAfter(0.75) { self.applicationState.lastSeenStatusId = lastSeenStatusId self.applicationState.amountOfNewStatuses = 0 } @@ -125,7 +126,7 @@ struct HomeFeedView: View { } if let account = self.applicationState.account { - _ = try await HomeTimelineService.shared.refreshTimeline(for: account) + _ = try await HomeTimelineService.shared.refreshTimeline(for: account, includeReblogs: self.applicationState.showReboostedStatuses) } self.applicationState.amountOfNewStatuses = 0 @@ -173,7 +174,7 @@ struct HomeFeedView: View { .resizable() .frame(width: 64, height: 64) .fontWeight(.ultraLight) - .foregroundColor(.accentColor.opacity(0.6)) + .foregroundColor(self.applicationState.tintColor.color().opacity(0.6)) Text("home.title.allCaughtUp", comment: "You're all caught up") .font(.title2) .fontWeight(.thin) diff --git a/Vernissage/Views/PaginableStatusesView.swift b/Vernissage/Views/PaginableStatusesView.swift index 6e6e29e..fd89f5c 100644 --- a/Vernissage/Views/PaginableStatusesView.swift +++ b/Vernissage/Views/PaginableStatusesView.swift @@ -81,7 +81,7 @@ struct PaginableStatusesView: View { private func list() -> some View { ScrollView { if self.imageColumns > 1 { - WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + WaterfallGrid($statusViewModels, refreshId: Binding.constant(""), columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) } onLoadMore: { do { diff --git a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift index 9f8112c..78c0a16 100644 --- a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift +++ b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift @@ -22,7 +22,11 @@ struct GeneralSectionView: View { "Orange-Camera", "Pride-Camera", "Yellow-Camera", - "Gradient-Camera"] + "Gradient-Camera", + "Orange-Lens", + "Pink-Lens", + "Blue-Lens", + "Brown-Lens"] private let themeNames: [(theme: Theme, name: LocalizedStringKey)] = [ (Theme.system, "settings.title.system"), diff --git a/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift b/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift index ffdd5e9..8fb41dc 100644 --- a/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift +++ b/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift @@ -85,6 +85,18 @@ struct MediaSettingsView: View { .onChange(of: self.applicationState.warnAboutMissingAlt) { newValue in ApplicationSettingsHandler.shared.set(warnAboutMissingAlt: newValue) } + + Toggle(isOn: $applicationState.showReboostedStatuses) { + VStack(alignment: .leading) { + Text("settings.title.enableReboostOnTimeline", comment: "Show boosted statuses") + Text("settings.title.enableReboostOnTimelineDescription", comment: "Boosted statuses will be visible on your home timeline.") + .font(.footnote) + .foregroundColor(.customGrayColor) + } + } + .onChange(of: self.applicationState.showReboostedStatuses) { newValue in + ApplicationSettingsHandler.shared.set(showReboostedStatuses: newValue) + } } } } diff --git a/Vernissage/Views/StatusView/StatusView.swift b/Vernissage/Views/StatusView/StatusView.swift index 054ba32..032f593 100644 --- a/Vernissage/Views/StatusView/StatusView.swift +++ b/Vernissage/Views/StatusView/StatusView.swift @@ -162,7 +162,7 @@ struct StatusView: View { } .padding(8) - CommentsSectionView(statusId: statusViewModel.id) + CommentsSectionView(statusId: statusViewModel.getOrginalStatusId()) } } .coordinateSpace(name: "scroll") diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index 590f27c..29178fe 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -52,6 +52,7 @@ struct StatusesView: View { @State private var statusViewModels: [StatusModel] = [] @State private var state: ViewState = .loading @State private var lastStatusId: String? + @State private var waterfallId: String = String.randomString(length: 8) // Gallery parameters. @State private var imageColumns = 3 @@ -96,7 +97,7 @@ struct StatusesView: View { private func list() -> some View { ScrollView { if self.imageColumns > 1 { - WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + WaterfallGrid($statusViewModels, refreshId: $waterfallId, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) } onLoadMore: { do { @@ -142,6 +143,17 @@ struct StatusesView: View { ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) } } + .onChange(of: self.applicationState.showReboostedStatuses) { _ in + if self.listType != .home { + return + } + + Task { @MainActor in + HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) + try await self.loadTopStatuses() + HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) + } + } } private func loadData() async { @@ -227,8 +239,12 @@ struct StatusesView: View { for item in statuses.getStatusesWithImagesOnly() { inPlaceStatuses.append(StatusModel(status: item)) } + + // Prefetch images. + self.prefetch(statusModels: inPlaceStatuses) // Replace old collection with new one. + self.waterfallId = String.randomString(length: 8) self.statusViewModels = inPlaceStatuses } @@ -239,7 +255,8 @@ struct StatusesView: View { maxId: maxId, sinceId: sinceId, minId: minId, - limit: self.defaultLimit) ?? [] + limit: self.defaultLimit, + includeReblogs: self.applicationState.showReboostedStatuses) ?? [] case .local: return try await self.client.publicTimeline?.getStatuses( local: true, diff --git a/Vernissage/Views/TrendStatusesView.swift b/Vernissage/Views/TrendStatusesView.swift index a0ccc9d..907ea91 100644 --- a/Vernissage/Views/TrendStatusesView.swift +++ b/Vernissage/Views/TrendStatusesView.swift @@ -90,7 +90,7 @@ struct TrendStatusesView: View { NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "trendingStatuses.title.noPhotos") } else { if self.imageColumns > 1 { - WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: Binding.constant(true)) { item in + WaterfallGrid($statusViewModels, refreshId: Binding.constant(""), columns: $imageColumns, hideLoadMore: Binding.constant(true)) { item in ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) } onLoadMore: { } } else { diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift index c62bd4d..c4500d7 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift @@ -42,7 +42,7 @@ struct UserProfileStatusesView: View { var body: some View { if firstLoadFinished == true { if self.imageColumns > 1 { - WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + WaterfallGrid($statusViewModels, refreshId: Binding.constant(""), columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in ImageRowAsync(statusViewModel: item, withAvatar: false, containerWidth: $containerWidth) } onLoadMore: { do { diff --git a/Vernissage/Widgets/ImageRowItem.swift b/Vernissage/Widgets/ImageRowItem.swift index af57c70..80cd3ae 100644 --- a/Vernissage/Widgets/ImageRowItem.swift +++ b/Vernissage/Widgets/ImageRowItem.swift @@ -61,10 +61,22 @@ struct ImageRowItem: View { } blurred: { ZStack { BlurredImage(blurhash: attachmentData.blurhash) - ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar) { - self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId, - accountDisplayName: self.status.accountDisplayName, - accountUserName: self.status.accountUsername)) + ImageAvatar(displayName: self.status.accountDisplayName, + avatarUrl: self.status.accountAvatar, + rebloggedAccountDisplayName: self.status.rebloggedAccountDisplayName, + rebloggedAccountAvatar: self.status.rebloggedAccountAvatar) { isAuthor in + if isAuthor { + self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId, + accountDisplayName: self.status.accountDisplayName, + accountUserName: self.status.accountUsername)) + } else { + if let rebloggedAccountId = self.status.rebloggedAccountId, + let rebloggedAccountUsername = self.status.rebloggedAccountUsername { + self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId, + accountDisplayName: self.status.rebloggedAccountDisplayName, + accountUserName: rebloggedAccountUsername)) + } + } } } .onTapGesture { @@ -141,10 +153,22 @@ struct ImageRowItem: View { ZStack { self.imageView(uiImage: uiImage) - ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar) { - self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId, - accountDisplayName: self.status.accountDisplayName, - accountUserName: self.status.accountUsername)) + ImageAvatar(displayName: self.status.accountDisplayName, + avatarUrl: self.status.accountAvatar, + rebloggedAccountDisplayName: self.status.rebloggedAccountDisplayName, + rebloggedAccountAvatar: self.status.rebloggedAccountAvatar) { isAuthor in + if isAuthor { + self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId, + accountDisplayName: self.status.accountDisplayName, + accountUserName: self.status.accountUsername)) + } else { + if let rebloggedAccountId = self.status.rebloggedAccountId, + let rebloggedAccountUsername = self.status.rebloggedAccountUsername { + self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId, + accountDisplayName: self.status.rebloggedAccountDisplayName, + accountUserName: rebloggedAccountUsername)) + } + } } ImageFavourite(isFavourited: $isFavourited) @@ -155,6 +179,23 @@ struct ImageRowItem: View { FavouriteTouch(showFavouriteAnimation: $showThumbImage) } } + + @ViewBuilder + func reblogInformation() -> some View { + if let rebloggedAccountAvatar = self.status.rebloggedAccountAvatar, + let rebloggedAccountDisplayName = self.status.rebloggedAccountDisplayName { + HStack(alignment: .center, spacing: 4) { + UserAvatar(accountAvatar: rebloggedAccountAvatar, size: .mini) + Text(rebloggedAccountDisplayName) + Image("custom.rocket") + .padding(.trailing, 8) + } + .font(.footnote) + .foregroundColor(Color.mainTextColor.opacity(0.4)) + .background(Color.mainTextColor.opacity(0.1)) + .clipShape(Capsule()) + } + } @ViewBuilder private func imageView(uiImage: UIImage) -> some View { @@ -228,7 +269,7 @@ struct ImageRowItem: View { private func navigateToStatus() { self.routerPath.navigate(to: .status( - id: status.rebloggedStatusId ?? status.id, + id: status.id, blurhash: status.attachments().first?.blurhash, highestImageUrl: status.attachments().getHighestImage()?.url, metaImageWidth: status.attachments().first?.metaImageWidth, diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift index 9d33dd8..a98a215 100644 --- a/Vernissage/Widgets/ImageRowItemAsync.swift +++ b/Vernissage/Widgets/ImageRowItemAsync.swift @@ -67,10 +67,21 @@ struct ImageRowItemAsync: View { BlurredImage(blurhash: attachment.blurhash) if self.showAvatar { ImageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis, - avatarUrl: self.statusViewModel.account.avatar) { - self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id, - accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis, - accountUserName: self.statusViewModel.account.acct)) + avatarUrl: self.statusViewModel.account.avatar, + rebloggedAccountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis, + rebloggedAccountAvatar: self.statusViewModel.reblogStatus?.account.avatar) { isAuthor in + if isAuthor { + self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id, + accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis, + accountUserName: self.statusViewModel.account.acct)) + } else { + if let rebloggedAccountId = self.statusViewModel.reblogStatus?.account.id, + let rebloggedAccountUsername = self.statusViewModel.reblogStatus?.account.acct { + self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId, + accountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis, + accountUserName: rebloggedAccountUsername)) + } + } } } } @@ -143,10 +154,21 @@ struct ImageRowItemAsync: View { if self.showAvatar { ImageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis, - avatarUrl: self.statusViewModel.account.avatar) { - self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id, - accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis, - accountUserName: self.statusViewModel.account.acct)) + avatarUrl: self.statusViewModel.account.avatar, + rebloggedAccountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis, + rebloggedAccountAvatar: self.statusViewModel.reblogStatus?.account.avatar) { isAuthor in + if isAuthor { + self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id, + accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis, + accountUserName: self.statusViewModel.account.acct)) + } else { + if let rebloggedAccountId = self.statusViewModel.reblogStatus?.account.id, + let rebloggedAccountUsername = self.statusViewModel.reblogStatus?.account.acct { + self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId, + accountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis, + accountUserName: rebloggedAccountUsername)) + } + } } } diff --git a/Vernissage/Widgets/InteractionRow.swift b/Vernissage/Widgets/InteractionRow.swift index 6b15530..f1a31b2 100644 --- a/Vernissage/Widgets/InteractionRow.swift +++ b/Vernissage/Widgets/InteractionRow.swift @@ -84,11 +84,11 @@ struct InteractionRow: View { Spacer() Menu { - NavigationLink(value: RouteurDestinations.accounts(listType: .reblogged(entityId: statusModel.id))) { + NavigationLink(value: RouteurDestinations.accounts(listType: .reblogged(entityId: statusModel.getOrginalStatusId()))) { Label("status.title.reboostedBy", image: "custom.rocket") } - NavigationLink(value: RouteurDestinations.accounts(listType: .favourited(entityId: statusModel.id))) { + NavigationLink(value: RouteurDestinations.accounts(listType: .favourited(entityId: statusModel.getOrginalStatusId()))) { Label("status.title.favouritedBy", systemImage: "star") } @@ -116,7 +116,7 @@ struct InteractionRow: View { Divider() Button { - self.routerPath.presentedSheet = .report(objectType: .post, objectId: self.statusModel.id) + self.routerPath.presentedSheet = .report(objectType: .post, objectId: self.statusModel.getOrginalStatusId()) } label: { Label(NSLocalizedString("status.title.report", comment: "Report"), systemImage: "exclamationmark.triangle") } @@ -144,8 +144,8 @@ struct InteractionRow: View { private func reboost() async { do { let status = self.reblogged - ? try await self.client.statuses?.unboost(statusId: self.statusModel.id) - : try await self.client.statuses?.boost(statusId: self.statusModel.id) + ? try await self.client.statuses?.unboost(statusId: self.statusModel.getOrginalStatusId()) + : try await self.client.statuses?.boost(statusId: self.statusModel.getOrginalStatusId()) if let status { self.reblogsCount = status.reblogsCount == self.reblogsCount @@ -166,8 +166,8 @@ struct InteractionRow: View { private func favourite() async { do { let status = self.favourited - ? try await self.client.statuses?.unfavourite(statusId: self.statusModel.id) - : try await self.client.statuses?.favourite(statusId: self.statusModel.id) + ? try await self.client.statuses?.unfavourite(statusId: self.statusModel.getOrginalStatusId()) + : try await self.client.statuses?.favourite(statusId: self.statusModel.getOrginalStatusId()) if let status { self.favouritesCount = status.favouritesCount == self.favouritesCount @@ -188,8 +188,8 @@ struct InteractionRow: View { private func bookmark() async { do { _ = self.bookmarked - ? try await self.client.statuses?.unbookmark(statusId: self.statusModel.id) - : try await self.client.statuses?.bookmark(statusId: self.statusModel.id) + ? try await self.client.statuses?.unbookmark(statusId: self.statusModel.getOrginalStatusId()) + : try await self.client.statuses?.bookmark(statusId: self.statusModel.getOrginalStatusId()) self.bookmarked.toggle() ToastrService.shared.showSuccess(self.bookmarked diff --git a/Vernissage/Widgets/WaterfallGrid.swift b/Vernissage/Widgets/WaterfallGrid.swift index 4206270..d097f90 100644 --- a/Vernissage/Widgets/WaterfallGrid.swift +++ b/Vernissage/Widgets/WaterfallGrid.swift @@ -13,11 +13,13 @@ struct WaterfallGrid: View where Data: RandomAccessCollection @Binding private var columns: Int @Binding private var hideLoadMore: Bool @Binding private var data: Data + @Binding private var refreshId: String private let content: (Data.Element) -> Content @State private var columnsData: [ColumnData] = [] @State private var processedItems: [Data.Element.ID] = [] + @State private var shouldRecalculate = false private let onLoadMore: () async -> Void private let semaphore = AsyncSemaphore(value: 1) @@ -46,8 +48,16 @@ struct WaterfallGrid: View where Data: RandomAccessCollection .onFirstAppear { self.recalculateArrays() } + .onChange(of: self.refreshId) { _ in + self.shouldRecalculate = true + } .onChange(of: self.data) { _ in - self.appendToArrays() + if self.shouldRecalculate { + self.recalculateArrays() + self.shouldRecalculate = false + } else { + self.appendToArrays() + } } .onChange(of: self.columns) { _ in self.recalculateArrays() @@ -113,25 +123,37 @@ struct WaterfallGrid: View where Data: RandomAccessCollection } extension WaterfallGrid { - init(_ data: Binding, id: KeyPath, columns: Binding, - hideLoadMore: Binding, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) { + init(_ data: Binding, + refreshId: Binding, + columns: Binding, + hideLoadMore: Binding, + content: @escaping (Data.Element) -> Content, + onLoadMore: @escaping () async -> Void) { + self.content = content self.onLoadMore = onLoadMore self._data = data self._columns = columns self._hideLoadMore = hideLoadMore + self._refreshId = refreshId } } extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable { - init(_ data: Binding, columns: Binding, - hideLoadMore: Binding, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) { + init(_ data: Binding, + refreshId: Binding, + columns: Binding, + hideLoadMore: Binding, + content: @escaping (Data.Element) -> Content, + onLoadMore: @escaping () async -> Void) { + self.content = content self.onLoadMore = onLoadMore self._data = data self._columns = columns self._hideLoadMore = hideLoadMore + self._refreshId = refreshId } } diff --git a/VernissageWidget/PhotoWidget/Service/StatusFetcher.swift b/VernissageWidget/PhotoWidget/Service/StatusFetcher.swift index b42057c..bc12481 100644 --- a/VernissageWidget/PhotoWidget/Service/StatusFetcher.swift +++ b/VernissageWidget/PhotoWidget/Service/StatusFetcher.swift @@ -27,7 +27,7 @@ public class StatusFetcher { } let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken) - let statuses = try await client.getHomeTimeline(limit: 20) + let statuses = try await client.getHomeTimeline(limit: 20, includeReblogs: defaultSettings.showReboostedStatuses) var widgetEntries: [PhotoWidgetEntry] = [] for status in statuses { diff --git a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift index 27b2b6c..52ca457 100644 --- a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift +++ b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift @@ -708,7 +708,7 @@ public struct BaseComposeView: View { } private func createStatus() -> Pixelfed.Statuses.Components { - return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.id, + return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.getOrginalStatusId(), text: self.textModel.text.string, spoilerText: self.isSensitive ? self.spoilerText : String.empty(), mediaIds: self.photosAttachment.getUploadedPhotoIds(), diff --git a/WidgetsKit/Sources/WidgetsKit/Widgets/ImageAvatar.swift b/WidgetsKit/Sources/WidgetsKit/Widgets/ImageAvatar.swift index 85e6b26..69af3fc 100644 --- a/WidgetsKit/Sources/WidgetsKit/Widgets/ImageAvatar.swift +++ b/WidgetsKit/Sources/WidgetsKit/Widgets/ImageAvatar.swift @@ -14,45 +14,63 @@ public struct ImageAvatar: View { private let displayName: String? private let avatarUrl: URL? - private let onTap: () -> Void - - public init(displayName: String?, avatarUrl: URL?, onTap: @escaping () -> Void) { + private let rebloggedAccountDisplayName: String? + private let rebloggedAccountAvatar: URL? + private let onTap: (Bool) -> Void + + public init(displayName: String?, avatarUrl: URL?, rebloggedAccountDisplayName: String?, rebloggedAccountAvatar: URL?, onTap: @escaping (Bool) -> Void) { self.displayName = displayName self.avatarUrl = avatarUrl + self.rebloggedAccountAvatar = rebloggedAccountAvatar + self.rebloggedAccountDisplayName = rebloggedAccountDisplayName self.onTap = onTap } public var body: some View { if self.applicationState.showAvatarsOnTimeline { - VStack(alignment: .leading) { - HStack(alignment: .center) { - HStack(alignment: .center) { - LazyImage(url: avatarUrl) { state in - if let image = state.image { - self.buildAvatar(image: image) - } else if state.isLoading { - self.buildAvatar() - } else { - self.buildAvatar() - } - } - + VStack(alignment: .leading, spacing: 0){ + HStack(alignment: .center, spacing: 0) { + HStack(alignment: .center, spacing: 4) { + UserAvatar(accountAvatar: avatarUrl, size: .mini) Text(displayName ?? "") - .font(.system(size: 15)) - .foregroundColor(.white.opacity(0.8)) .fontWeight(.semibold) - .shadow(color: .black, radius: 2) + .lineLimit(1) + .padding(.trailing, 8) } - .padding(8) + .font(.footnote) + .foregroundColor(.white.opacity(0.8)) + .background(.black.opacity(0.6)) + .clipShape(Capsule()) + .padding(.leading, 8) + .padding(.top, 8) .onTapGesture { - self.onTap() + self.onTap(true) + } + + if let rebloggedAccountAvatar = self.rebloggedAccountAvatar, + let rebloggedAccountDisplayName = self.rebloggedAccountDisplayName { + HStack(alignment: .center, spacing: 4) { + UserAvatar(accountAvatar: rebloggedAccountAvatar, size: .mini) + Text(rebloggedAccountDisplayName) + .lineLimit(1) + Image("custom.rocket") + .padding(.trailing, 8) + } + .font(.footnote) + .foregroundColor(.white.opacity(0.8)) + .background(.black.opacity(0.6)) + .clipShape(Capsule()) + .padding(.leading, 8) + .padding(.top, 8) + .onTapGesture { + self.onTap(false) + } } - Spacer() } - Spacer() } + .padding(.trailing, 58) } }